PageSourceSearch

https://usmcocne.org/static/web.js

js usmcocne.org collected 2026-10-05 00:58:13 UTC 605,643 bytes, 21,139 lines download raw bytes

1
2//// File web.js ////
3'use strict'
4
5function startApp() {
6
7    $angular.modules = [ "ngRoute", "ngCookies", "ngTouch", "ui.bootstrap",
8		  "pascalprecht.translate", "ngSanitize", "ngAnimate", 'angular-bind-html-compile', 'thatisuday.dropzone', 
9		  '720kb.datepicker', "ngMap", "ui.select", "monospaced.elastic", "youtube-embed" ];
10  
11	$angular.init();
12
13	// init root scope
14	$angular.app.run(function($rootScope, $location, $http, $window, $timeout, $templateCache) {
15		$init.log("start run web");
16		$angular.initRootScope($rootScope, $location, $window, $timeout, true);
17		
18		$rootScope.getItemLinkBy = function(o) {
19			var entity = o.sys_entity ? o.sys_entity : o.entity;
20			var id = o.sys_id ? o.sys_id : (o.id ? o.id : (o._id ? o._id : o.entity_id));
21			return $rootScope.getItemLink(entity, id);
22		};
23		
24		$rootScope.getItemLink = function(entity, id) {
25			if (entity === 'instance') {
26				return contextPath + '/home/' + id;
27			}
28			var url = $instance.getBaseUrl() + entity;
29			if (id != null) {
30				url += "/" + id;
31			}
32			return url;
33		};
34		
35		$init.log("end run");
36	});
37
38	$angular.app.controller('header', function($rootScope, $scope) {
39		$angular.headerScope = $scope;
40
41		$rootScope.openSearch = function(e) {
42			$utils.preventDefault(e);
43			
44			$("body").addClass("searching");
45			var $form = $('#search')
46			$form.addClass('active');
47			setTimeout(function() {
48				$form.find('input')[0].focus();
49			}, 200);
50			
51			$rootScope.search();
52		};
53		
54		$rootScope.closeSearch = function() {
55			var $body = $("body");
56			if (!$body.hasClass("searching")) {
57				return;
58			}
59			$body.removeClass("searching");
60	        var $form = $('#search.active');
61			$form.removeClass('active');
62		};
63		
64		$rootScope.searchList = [];
65		$rootScope.search = function(e) {
66			if (e && e.keyCode === 27) {
67				$rootScope.closeSearch();
68				return;
69			}
70
71			var txt = $rootScope.mysearch;
72			if ($utils.isBlank(txt)) {
73				$rootScope.searchList = [];
74				return;
75			}
76			txt = $utils.removeDiacritics(txt.trim().toLowerCase());
77			
78			// generic search
79			$rest.get({
80				url: contextPath + "/www/instance/"+$instance.getId()+"/search.json?_q=" + txt 
81			}).then(function(o) {
82				$rootScope.searchList = o.list;
83			});
84		};
85		
86		$rootScope.clickGroup = function(e, page) {
87			$utils.preventDefault(e);
88			if (page.open == null) page.open = false;
89			page.open = !page.open;
90		};
91		
92	});
93
94	$angular.app.controller('menu', function($rootScope, $scope) {
95		$angular.menuScope = $scope;
96		$scope.selectedPage = "";
97		$scope.swipeLeft = function() {
98			$events.toggleMenu();
99		};
100	});
101
102	$angular.app.controller('cartAsMenu', function($scope, $location) {
103		$angular.menuScope = $scope;
104		$scope.goToCheckout = function() {
105			$location.path($instance.getBaseUrl() + 'cart');
106		}
107	});
108
109	$angular.app.controller('cookies', function($rootScope, $scope) {
110		// cookies message
111		var version = "2";
112		var cookieMsg = $storageService.get("_cookies");
113		if (cookieMsg != version) {
114			$("#cookies").removeClass("hidden");
115
116			$scope.accept = function() {
117				$angular.acceptLicense('cookies', null, function() {
118					$("#cookies").addClass("hidden");
119					$storageService.put("_cookies", version);
120					$angular.initGA();
121				});
122			}
123		} else {
124			$angular.initGA();
125		}
126	});
127	
128	$angular.prepareRouteProvider = function($routeProvider) {
129
130		if ($utils.isBlank(appInstance)) {
131	    	$routeProvider.when('/', $angular.resolveRoute('web-select'));
132	    	$routeProvider.when('/home', $angular.resolveRoute('web-select'));
133		} else {
134			$routeProvider.when('/', $angular.resolveRoute('web-home'));
135			$routeProvider.when('/home', $angular.resolveRoute('web-home'));
136	    	$routeProvider.when('/p/:id', $angular.resolveRoute('web-home'));
137		}
138		
139    	$routeProvider.when('/home/:instanceId', $angular.resolveRoute('web-home'));
140    	$routeProvider.when('/home/:instanceId/p/:id', $angular.resolveRoute('web-home'));
141		
142    	$routeProvider.when('/home/:instanceId/preferences', $angular.resolveRoute('preferences', false, true));
143    	$routeProvider.when('/home/:instanceId/login', $angular.resolveRoute('login', false, true));
144    	$routeProvider.when('/home/:instanceId/logout', $angular.resolveRoute('logout', false, true));
145    	$routeProvider.when('/home/:instanceId/plan', $angular.resolveRoute('web-plan'));
146		$routeProvider.when('/home/:instanceId/agenda', $angular.resolveRoute('web-agenda'));
147    	$routeProvider.when('/home/:instanceId/user', $angular.resolveRoute('web-user', true));
148		$routeProvider.when('/home/:instanceId/pos', $angular.resolveRoute('web-pos', true));
149		$routeProvider.when('/home/:instanceId/cart', $angular.resolveRoute('web-cart'));
150    	$routeProvider.when('/home/:instanceId/orders', $angular.resolveRoute('web-orders', true));
151		$routeProvider.when('/home/:instanceId/order/:id', $angular.resolveRoute('web-order'));
152		$routeProvider.when('/home/:instanceId/bookings', $angular.resolveRoute('web-bookings', true));
153		$routeProvider.when('/home/:instanceId/booking/:id', $angular.resolveRoute('web-booking', true));
154		$routeProvider.when('/home/:instanceId/:entity', $angular.resolveRoute('web-list'));
155		$routeProvider.when('/home/:instanceId/:entity/:entityId', $angular.resolveRoute('web-item'));
156		$routeProvider.when('/home/:instanceId/:entity/:entityId/pay', $angular.resolveRoute('web-cart'));
157		
158    	$routeProvider.when('/preferences', $angular.resolveRoute('preferences', false, true));
159    	$routeProvider.when('/login', $angular.resolveRoute('login', false, true));
160    	$routeProvider.when('/logout', $angular.resolveRoute('logout', false, true));
161    	$routeProvider.when('/plan', $angular.resolveRoute('web-plan'));
162		$routeProvider.when('/agenda', $angular.resolveRoute('web-agenda'));
163    	$routeProvider.when('/user', $angular.resolveRoute('web-user', true));
164		$routeProvider.when('/pos', $angular.resolveRoute('web-pos', true));
165		$routeProvider.when('/cart', $angular.resolveRoute('web-cart'));
166    	$routeProvider.when('/orders', $angular.resolveRoute('web-orders', true));
167		$routeProvider.when('/order/:id', $angular.resolveRoute('web-order', true));
168		$routeProvider.when('/bookings', $angular.resolveRoute('web-bookings', true));
169		$routeProvider.when('/booking/:id', $angular.resolveRoute('web-booking', true));
170		$routeProvider.when('/:entity', $angular.resolveRoute('web-list'));
171		$routeProvider.when('/:entity/:entityId', $angular.resolveRoute('web-item'));
172		$routeProvider.when('/:entity/:entityId/pay', $angular.resolveRoute('web-cart'));
173		
174		$routeProvider.otherwise($angular.resolveRoute('not-found'));
175    };
176
177    $angular.start();
178};
179
180//// File i18n.js ////
181var $i18n = {
182
183	"currencies": {
184		"USD" : {
185			"name": "US Dollars",
186			"symbol": "$",
187			"symbolAfterNumber": false,
188			"decimalDigits": 2,
189			"decimalNameKey": "msg.currency.USD.decimals"
190		},
191		"EUR" : {
192			"name": "Euros",
193			"symbol": "€",
194			"symbolAfterNumber": true,
195			"decimalDigits": 2,
196			"decimalNameKey": "msg.currency.EUR.decimals"
197		},
198		"MXN" : {
199			"name": "Pesos Mexicanos",
200			"symbol": "$",
201			"symbolAfterNumber": false,
202			"decimalDigits": 2,
203			"decimalNameKey": "msg.currency.MXN.decimals"
204		}
205	},
206	
207	"messages": {
208		"en" : {
209			// GENERIC MESSAGES
210			"msg.error": "¡Oops! Something went wrong.",
211			"msg.error.noConnection": "No internet connection",
212			"msg.error.502" : "The server is not available. Please try again in a moment.",
213			"msg.notFound": "Resource not found",
214			"msg.accessDenied": "You don't have permission to access to this resource.",
215			"msg.tooManyAttempts": "Too many attempts. Please, wait just a moment and try it again.",
216			"msg.data.conflict": "Please refresh the page due to a data conflict.",
217			"msg.data.invalid" : "Incorrect data. Please check your details.",
218			"msg.role.notAllowed" : "Role not allowed",
219			"msg.invalid": "Invalid",
220			"msg.noDataFound" : "No data found",
221		
222			"msg.language": "Language",
223			"msg.lang.en": "English",
224			"msg.lang.es": "Spanish",
225			"msg.lang.fr": "French",
226		
227			"role.super": "Super user",
228			"role.admin": "Administrator",
229			"role.coordinator": "Coordinator",
230			"role.basic": "Associated user",
231			
232			"msg.cybersource" : "Credit/debit card",
233		
234			"msg.path": "Path",
235			"msg.title": "Title",
236			"msg.subtitle": "Subtitle",
237			"msg.status": "Status",
238			"msg.code": "Code",
239			"msg.name": "Name",
240			"msg.name.extra": "Suffix",
241			"msg.key": "Key",
242			"msg.value": "Value",
243			"msg.text": "Text",
244			"msg.type": "Type",
245			"msg.color": "Color",
246			"msg.priority": "Priority",
247			"msg.email": "Email",
248			"msg.phone": "Phone",
249			"msg.phoneInt": "International phone",
250			"msg.description": "Description",
251			"msg.comments": "Comments",
252			"msg.address": "Address",
253			"msg.hint": "Hint",
254			"msg.username": "Username",
255			"msg.usernameOrEmail": "Username or email",
256			"msg.password": "Password",
257			"msg.password.new": "Password new",
258			"msg.sortBy": "Sort by",
259			"msg.enabled": "Enabled",
260			"msg.disabled": "Disabled",
261			"msg.tags": "Tags",
262			"msg.category": "Category",
263			"msg.categories": "Categories",
264			"msg.category.types": "Categories by entity",
265			"msg.category.parents": "Parents",
266			"msg.category.children": "Children",
267			"msg.category.notExisting": "Not existing",
268			"msg.date": "Date",
269			"msg.datetime": "Date time",
270			"msg.time": "Time",
271			"msg.day": "Day",
272			"msg.empty": "Empty",
273			"msg.keywords": "Keywords",
274			"msg.radius": "Radius",
275			"msg.customId": "Custom Id",
276			"msg.customId.placeholder": "Use a self-describing id (e.g. first letters of name)",
277			"msg.role": "Role",
278			"msg.morning": "Morning",
279			"msg.afternoon": "Afternoon",
280			"msg.night": "Night",
281			"msg.skipped": "Skipped",
282			"msg.behavior": "Behavior",
283			"msg.total": "Total",
284			"msg.subtotal": "Subtotal",
285			"msg.fixed": "Fixed",
286			"msg.url": "Url",
287			"msg.elements": "elements",
288			"msg.dashboard": "Dashboard",
289			"msg.spreadsheet": "Spreadsheet",
290			"msg.recovered": "Recovered",
291			"msg.inRisk": "In Risk",
292			"msg.sick": "Sick",
293			"msg.suspicious": "Suspicious",
294			"msg.actions": "Actions",
295			"msg.products": "Products",
296			"msg.employee": "Employee",
297			"msg.inheritance": "Inheritance",
298			"msg.plusDays": "Plus days",
299			"msg.more": "More",
300			"msg.less": "Less",
301			"msg.reloadInSeconds": "Reload in {{seconds}} seconds",
302			"msg.showMoreOptions": "show more options",
303			"msg.alignment" : "Alignment",
304			"msg.queryBuilder" : "Query builder",
305			"msg.filters" : "Filters",
306			"msg.filters.add" : "Add filter",
307			"msg.limit" : "Limit (Max number of rows)",
308			"msg.sort" : "Sort",
309			"msg.sort.add" : "Add sort",
310			"msg.direction" : "Direction",
311			"msg.direction.asc" : "Asc",
312			"msg.direction.desc" : "Desc",
313			"msg.projection" : "Projection (Data needed from database)",
314			"msg.projection.add" : "Add projection column",
315			"msg.query" : "Query",
316			"msg.query.empty": "no query built",
317
318			"msg.formula": "Formula",
319			"msg.formula.argument": "Argument",
320			"msg.formula.argument.add": "Add another argument",
321			"msg.formular.optional": "optional",
322			"msg.formula.empty": "no formula built",
323			"msg.formula.builder": "Formula builder",
324			"msg.formula.func.display.desc": "Get value from column (or path) to show",
325			"msg.formula.func.math.desc": "Do arithmetic operations",
326			"msg.formula.func.string.desc": "Parse numbers, booleans... to string",
327			"msg.formula.func.number.desc": "Parse string to number",
328			"msg.formula.func.bool.desc": "Parse string (True, yes...) or number (1/0) to boolean",
329			"msg.formula.func.not.desc": "Negate boolean value: true => false or false => true",
330			"msg.formula.func.and.desc": "Return true only if all booleans are true",
331			"msg.formula.func.or.desc": "Return true if any of booleans are true",
332			"msg.formula.func.eq.desc": "Return true if two values are equals",
333			"msg.formula.func.ne.desc": "Return true if two values are different",
334			"msg.formula.func.gt.desc": "Return true if first value is greater than second value",
335			"msg.formula.func.gte.desc": "Return true if first value is greater than second value or are equals",
336			"msg.formula.func.lt.desc": "Return true if first value is less than second value",
337			"msg.formula.func.lte.desc": "Return true if first value is less than second value or are equals",
338			"msg.formula.func.empty.desc": "Return true if value is empty string/list or value is null",
339			"msg.formula.func.length.desc": "Return number of characters in a string or number of items in a list",
340			"msg.formula.func.replace.desc": "Replace a text in a string by another text",
341			"msg.formula.func.substring.desc": "Get part of a string",
342			"msg.formula.func.contains.desc": "Check if text contains another text",
343			"msg.formula.func.round.desc": "Round a number",
344			"msg.formula.func.round_ceil.desc": "Round to ceil a number",
345			"msg.formula.func.round_floor.desc": "Round to floor a number",
346			"msg.formula.func.sum.desc": "Add all numbers from a list",
347			"msg.formula.func.avg.desc": "Calculate average of all numbers from a list",
348			"msg.formula.func.if.desc": "Return some value if a condition is true or another value if it's false",
349			"msg.formula.func.convert_to_text.desc": "Write a number with words",
350			"msg.formula.arg.operation": "Apply operations to fix numbers or columns with numbers.<br />Available operators: <strong>&nbsp;&nbsp;&nbsp;+&nbsp;&nbsp;&nbsp;-&nbsp;&nbsp;&nbsp;*&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;div&nbsp;&nbsp;&nbsp;mod</strong><br />Examples: <strong>5 + 8</strong> , <strong>age - 10</strong> , <strong>price * amount</strong>",
351			"msg.formula.arg.decimals": "Number of decimals to show",
352
353			"msg.operator.eq": "is equal to",
354			"msg.operator.ne": "is not equal to",
355			"msg.operator.gt": "is greater than",
356			"msg.operator.gte": "is greater or equal to",
357			"msg.operator.lt": "is less than",
358			"msg.operator.lte": "is less or equal to",
359			"msg.operator.in": "is in",
360			"msg.operator.nin": "is not in",
361			"msg.operator.like": "is like",
362			"msg.operator.exists": "exists",
363
364			"msg.dashboard.map.title": "COVID-19 cases map",
365			"msg.dashboard.map.description": "It displays confirmed cases location",
366			"msg.dashboard.graph.title": "COVID-19 cases graph",
367			"msg.dashboard.graph.description": "It displays daily cumulative cases",
368
369			"msg.datamixer.builder": "Datamixer Builder",
370			"msg.datamixer.addFromEntity": "Add data from entity",
371			"msg.datamixer.addFromVar": "Transform data from",
372			"msg.datamixer.variable": "Variable",
373			"msg.datamixer.process": "Process data",
374			"msg.datamixer.fromItem": "From item",
375			"msg.datamixer.fromItemPath": "From item <strong>{{from}}</strong>",
376			"msg.datamixer.fromEntity": "From entity <strong>{{from}}</strong>",
377			"msg.datamixer.fromVariable": "From variable <strong>{{from}}</strong>",
378			"msg.datamixer.storeData": "Store data",
379			"msg.datamixer.storedInVariable": "Data stored in variable",
380			"msg.datamixer.newVariable": "Name of variable where store processed data",
381			"msg.datamixer.fromData": "From data",
382			"msg.datamixer.toData": "Result after execute operation",
383			"msg.datamixer.function": "Function",
384			"msg.datamixer.operation": "Operation",
385			"msg.datamixer.operations": "Operations to be applied",
386			"msg.datamixer.addOperation": "Add operation",
387			"msg.datamixer.sampleData": "Sample data",
388			"msg.datamixer.removeOperation": "Remove operation",
389			"msg.datamixer.removeData": "Remove variable and its operations",
390			"msg.datamixer.variableAlreadyExists": "Variable name already exists",
391			"msg.datamixer.invalidVariableName": "Variable name must only have alphanumeric characters and underscores",
392			"msg.datamixer.by": "by",
393			"msg.datamixer.from": "from",
394			"msg.datamixer.atPosition": "at position",
395			"msg.datamixer.valueOf": "value of",
396			"msg.datamixer.num": "num",
397			"msg.datamixer.property": "property",
398			"msg.datamixer.regex": "regular expression",
399			"msg.datamixer.times": "times",
400			"msg.datamixer.loadFormulas": "calculate formulas",
401			"msg.datamixer.changeToExpression": "change to expression",
402			"msg.datamixer.changeToValue": "change to value",
403			"msg.datamixer.operation.count": "Count",
404			"msg.datamixer.operation.count.list": "<strong>Count</strong> items",
405			"msg.datamixer.operation.count.string": "<strong>Count</strong> characters",
406			"msg.datamixer.operation.sum": "Sum",
407			"msg.datamixer.operation.sum.list": "<strong>Sum</strong> all items",
408			"msg.datamixer.operation.filter.list": "<strong>Filter</strong> items",
409			"msg.datamixer.operation.filter.list.params": "<strong>Filter</strong> items by <strong>{{params.path}}</strong> {{operator}} <strong>{{filterValue}}</strong>",
410			"msg.datamixer.operation.query.list": "Apply <strong>query</strong> to items",
411			"msg.datamixer.operation.query.list.params": "Apply to items the <strong>query</strong>: <strong>{{params.query}}</strong>",
412			"msg.datamixer.operation.group.list": "<strong>Group</strong> items",
413			"msg.datamixer.operation.group.list.params": "<strong>Group</strong> items by <strong>{{params.by}}</strong>",
414			"msg.datamixer.operation.sort.list": "<strong>Sort</strong> items",
415			"msg.datamixer.operation.sort.list.params": "<strong>Sort</strong> items by <strong>{{params.by}}</strong>",
416			"msg.datamixer.operation.get.object": "<strong>Get</strong> value",
417			"msg.datamixer.operation.get.object.params": "<strong>Get</strong> value of <strong>{{params.at}}</strong>",
418			"msg.datamixer.operation.get.list": "<strong>Get</strong> item",
419			"msg.datamixer.operation.get.list.params": "<strong>Get</strong> item at position <strong>{{params.at}}</strong>",
420			"msg.datamixer.operation.get.string": "<strong>Get</strong> character",
421			"msg.datamixer.operation.get.string.params": "<strong>Get</strong> character at position <strong>{{params.at}}</strong>",
422			"msg.datamixer.operation.add.day": "<strong>Add</strong> days",
423			"msg.datamixer.operation.add.day.params": "<strong>Add</strong> <strong>{{params.value}}</strong> days",
424			"msg.datamixer.operation.add.time": "<strong>Add</strong> minutes",
425			"msg.datamixer.operation.add.time.params": "<strong>Add</strong> <strong>{{params.value}}</strong> minutes",
426			"msg.datamixer.operation.add.object": "<strong>Add</strong> property",
427			"msg.datamixer.operation.add.object.params": "<strong>Add</strong> property <strong>{{params.new_var}}</strong> with:",
428			"msg.datamixer.operation.add.list": "<strong>Add</strong> to each item",
429			"msg.datamixer.operation.add.list.params": "<strong>Add</strong> to each item:",
430			"msg.datamixer.operation.add.list.object.params": "<strong>Add</strong> property <strong>{{params.new_var}}</strong> to each item:",
431			"msg.datamixer.operation.add.integer": "<strong>Add</strong> <strong>{{params.value}}</strong>",
432			"msg.datamixer.operation.add.decimal": "<strong>Add</strong> <strong>{{params.value}}</strong>",
433			"msg.datamixer.operation.add.string": "<strong>Add</strong> text",
434			"msg.datamixer.operation.add.string.params": "<strong>Add</strong> text <strong>{{params.value}}</strong> to end",
435			"msg.datamixer.operation.subtract.day": "<strong>Subtract</strong> days",
436			"msg.datamixer.operation.subtract.day.params": "<strong>Subtract</strong> <strong>{{params.value}}</strong> days",
437			"msg.datamixer.operation.subtract.time": "<strong>Subtract</strong> minutes",
438			"msg.datamixer.operation.subtract.time.params": "<strong>Subtract</strong> <strong>{{params.value}}</strong> minutes",
439			"msg.datamixer.operation.subtract.object": "<strong>Remove</strong> property",
440			"msg.datamixer.operation.subtract.object.params": "<strong>Remove</strong> property <strong>{{params.value}}</strong>",
441			"msg.datamixer.operation.subtract.list": "<strong>Subtract</strong> to each item",
442			"msg.datamixer.operation.subtract.list.params": "<strong>Subtract</strong> to each item by <strong>{{params.value}}</strong>",
443			"msg.datamixer.operation.subtract.integer": "<strong>Subtract</strong> <strong>{{params.value}}</strong>",
444			"msg.datamixer.operation.subtract.decimal": "<strong>Subtract</strong> <strong>{{params.value}}</strong>",
445			"msg.datamixer.operation.subtract.string": "<strong>Subtract</strong> text",
446			"msg.datamixer.operation.subtract.string.params": "<strong>Subtract</strong> text matched by regular expression <strong>{{params.value}}</strong>",
447			"msg.datamixer.operation.multiply.list": "<strong>Multiply</strong> to each item",
448			"msg.datamixer.operation.multiply.list.params": "<strong>Multiply</strong> to each item by <strong>{{params.value}}</strong>",
449			"msg.datamixer.operation.multiply.integer": "<strong>Multiply</strong> <strong>{{params.value}}</strong>",
450			"msg.datamixer.operation.multiply.decimal": "<strong>Multiply</strong> <strong>{{params.value}}</strong>",
451			"msg.datamixer.operation.divide.list": "<strong>Divide</strong> to each item",
452			"msg.datamixer.operation.divide.list.params": "<strong>Divide</strong> to each item by <strong>{{params.value}}</strong>",
453			"msg.datamixer.operation.divide.integer": "<strong>Divide</strong> <strong>{{params.value}}</strong>",
454			"msg.datamixer.operation.divide.decimal": "<strong>Divide</strong> <strong>{{params.value}}</strong>",
455			"msg.datamixer.operation.replace.list": "<strong>Replace</strong> each item",
456			"msg.datamixer.operation.replace.list.params": "<strong>Replace</strong> each item by:",
457			"msg.datamixer.operation.fetch.list": "<strong>Fetch</strong> items",
458			"msg.datamixer.operation.fetch.list.params": "<strong>Fetch</strong> items {{params.load_formulas ? '& calculate formulas' : ''}}",
459			"msg.datamixer.operation.fetch.string": "<strong>Fetch</strong> to entity <strong>{{params.entity}}</strong>",
460			"msg.datamixer.operation.clone.day": "<strong>Duplicate</strong> day",
461			"msg.datamixer.operation.clone.day.params": "<strong>Duplicate</strong> day <strong>{{params.times}}</strong> times",
462			"msg.datamixer.operation.clone.time": "<strong>Duplicate</strong> time",
463			"msg.datamixer.operation.clone.time.params": "<strong>Duplicate</strong> time <strong>{{params.times}}</strong> times",
464			"msg.datamixer.operation.clone.object": "<strong>Duplicate</strong> object",
465			"msg.datamixer.operation.clone.object.params": "<strong>Duplicate</strong> object <strong>{{params.times}}</strong> times",
466			"msg.datamixer.operation.clone.list": "<strong>Duplicate</strong> list",
467			"msg.datamixer.operation.clone.list.params": "<strong>Duplicate</strong> list <strong>{{params.times}}</strong> times",
468			"msg.datamixer.operation.clone.integer": "<strong>Duplicate</strong> number",
469			"msg.datamixer.operation.clone.integer.params": "<strong>Duplicate</strong> number <strong>{{params.times}}</strong> times",
470			"msg.datamixer.operation.clone.decimal": "<strong>Duplicate</strong> number",
471			"msg.datamixer.operation.clone.decimal.params": "<strong>Duplicate</strong> number <strong>{{params.times}}</strong> times",
472			"msg.datamixer.operation.clone.string": "<strong>Duplicate</strong> text",
473			"msg.datamixer.operation.clone.string.params": "<strong>Duplicate</strong> text <strong>{{params.times}}</strong> times",
474			"msg.datamixer.operation.clone.boolean": "<strong>Duplicate</strong> boolean",
475			"msg.datamixer.operation.clone.boolean.params": "<strong>Duplicate</strong> boolean <strong>{{params.times}}</strong> times",
476			"msg.datamixer.operation.wrap": "<strong>Wrap</strong> in object",
477			"msg.datamixer.operation.wrap.params": "<strong>Wrap</strong> in object property <strong>{{params.new_var}}</strong>",
478			"msg.datamixer.operation.unwrap.list": "<strong>Unwrap</strong> list of lists",
479			"msg.datamixer.operation.formula.object": "Execute <strong>formula</strong>",
480			"msg.datamixer.operation.formula.object.params": "Execute formula: <strong>{{params.formula}}</strong>",
481			"msg.datamixer.operation.formula.list": "Execute <strong>formula</strong> on each item",
482			"msg.datamixer.operation.formula.list.params": "Execute formula on each item: <strong>{{params.formula}}</strong>",
483			"msg.datamixer.operation.invalid": "Function <strong>{{function}}</strong> cannot be applied to data type <strong>{{from_type}}</strong>",
484			"msg.datamixer.component.new": "New component",
485			"msg.datamixer.component.edit": "Edit component",
486			"msg.datamixer.component.wizard": "Component Wizard",
487			"msg.datamixer.component.type.choose": "Choose component type",
488			"msg.datamixer.component.icon.choose": "Icon: choose from <a href=\"https://fonts.google.com/icons\" target=\"_blank\">Material Icons</a>",
489			"msg.datamixer.component.value.choose": "Define value to show",
490			"msg.datamixer.component.style.choose": "Modify component styles",
491			"msg.datamixer.component.series.choose": "Define data series",
492			"msg.datamixer.component.markers.choose": "Select geo located data",
493			"msg.datamixer.component.tableData.choose": "Define data to show on table",
494			"msg.datamixer.component.operation.choose": "Grouped data operation",
495			"msg.datamixer.component.innerEntity": "Inner entity",
496			"msg.datamixer.component.innerEntity.path": "Path",
497			"msg.datamixer.component.color": "Color",
498			"msg.datamixer.component.width": "Width (12 is full width)",
499			"msg.datamixer.component.type.counter": "Counter",
500			"msg.datamixer.component.type.chart": "Chart",
501			"msg.datamixer.component.type.map": "Map",
502			"msg.datamixer.component.type.table": "Table",
503			"msg.datamixer.component.counter.value": "Values column (or path)",
504			"msg.datamixer.component.chart.type": "Chart type",
505			"msg.datamixer.component.chart.type.line": "Line",
506			"msg.datamixer.component.chart.type.area": "Area",
507			"msg.datamixer.component.chart.type.bar": "Bar",
508			"msg.datamixer.component.chart.type.radar": "Radar",
509			"msg.datamixer.component.chart.type.histogram": "Histogram",
510			"msg.datamixer.component.chart.type.pie": "Pie",
511			"msg.datamixer.component.chart.type.donut": "Donut",
512			"msg.datamixer.component.chart.type.radialBar": "Radial bar",
513			"msg.datamixer.component.chart.type.scatter": "Scatter",
514			"msg.datamixer.component.chart.type.heatmap": "HeatMap",
515			"msg.datamixer.component.chart.series": "Indicate one or more series",
516			"msg.datamixer.component.chart.series.name": "Serie name",
517			"msg.datamixer.component.chart.series.serie": "Serie values column (or path)",
518			"msg.datamixer.component.chart.series.serie.add": "Add another serie",
519			"msg.datamixer.component.chart.series.labels": "Labels column (or path)",
520			"msg.datamixer.component.chart.axis.x": "X axis column (or path)",
521			"msg.datamixer.component.chart.group": "Group data",
522			"msg.datamixer.component.chart.group.by": "By",
523			"msg.datamixer.component.chart.operation": "Operation",
524			"msg.datamixer.component.chart.timeline": "Timeline",
525			"msg.datamixer.component.chart.timeline.range": "Show data from last",
526			"msg.datamixer.component.chart.timeline.minutes": "Minutes",
527			"msg.datamixer.component.chart.timeline.hours": "Hours",
528			"msg.datamixer.component.chart.timeline.days": "Days",
529			"msg.datamixer.component.chart.timeline.weeks": "Weeks",
530			"msg.datamixer.component.chart.timeline.months": "Months",
531			"msg.datamixer.component.chart.timeline.years": "Years",
532			"msg.datamixer.component.chart.pie.portionsByColumns": "Portions by columns",
533			"msg.datamixer.component.chart.pie.portions.add": "Add portion",
534			"msg.datamixer.component.chart.pie.portion.label": "Portion label",
535			"msg.datamixer.component.chart.pie.portion.path": "Path to field",
536			"msg.datamixer.component.table.columns": "Table columns",
537			"msg.datamixer.component.table.columns.path": "Path to field",
538			"msg.datamixer.component.table.columns.label": "Column title",
539			"msg.datamixer.component.table.columns.add": "Add another columns",
540			"msg.datamixer.component.map.labels": "Markers title column (or path)",
541			"msg.datamixer.userInputs.sampleData": "Fill data to be used in samples",
542			"msg.datamixer.error.tooMuchTime": "Calculations take too much time. Contact systems department to improve processes.",
543		
544			"msg.all": "All",
545			"msg.show": "Show",
546			"msg.required": "Required",
547			"msg.app_object": "App object",
548			"msg.listing": "Listing",
549			"msg.link": "Link",
550			"msg.noPlan": "No plan",
551			"msg.noShadow": "No shadow",
552			"msg.font": "Main font",
553			"msg.fontDisplay": "Special font",
554			"msg.rounded": "Rounded",
555			"msg.roundedBtns": "Rounded buttons",
556			"msg.disableDefault" : "Disable default template",
557			"msg.styles": "Styles / css",
558			"msg.cssImports": "CSS imports",
559			"msg.css.settings": "Settings",
560			"msg.css.variables": "Page and content styles",
561			"msg.css.imports": "Import fonts or styles",
562			"msg.css.sections": "Section styles",
563			"msg.css.editor": "CSS Editor",
564			"msg.variables": "Variables",
565			"msg.title.plan": "Plan title",
566			"msg.title.result": "Result title",
567			"msg.extension": "Extension",
568			"msg.trash": "Trash",
569			"msg.ips": "IPs",
570			"msg.level": "Level",
571			"msg.sys_changes": "Change history",
572			"msg.change.path": "Modified element",
573			"msg.change.old": "Before",
574			"msg.change.new": "After",
575			"msg.show.in.map": "Show in map",
576			"msg.sync": "Sync",
577			"msg.public": "Public",
578			"msg.uploadImage": "Upload image",
579			"msg.uploadAudio": "Upload audio",
580			"msg.uploadVideo": "Upload video",
581			"msg.uploadFile": "Upload file",
582			"msg.notify": "Notify",
583			"msg.set_value": "Set value",
584			"msg.copy_to_column": "Copy to column",
585			"msg.multiActions": "Multiple actions",
586			"msg.saveBeforeExecute": "Save before execute",
587			"msg.stock": "Stock",
588			"msg.stock.exceeded": "Out of stock",
589			"msg.stock.show": "Show stock",
590			"msg.multiple": "Multiple",
591			"msg.youtube.url": "YouTube URL",
592			"msg.playlist": "Playlist",
593			"msg.property": "Property",
594			"msg.col": "Column",
595			"msg.row": "Row",
596			"msg.colNum": "Column number",
597			"msg.rowNum": "Row number",
598			"msg.customFilters": "Custom filters",
599			"msg.spreadsheet.query": "Query to apply always",
600			"msg.spreadsheet.query.sample": "example: name:like=john&age:$gt=18",
601
602			"msg.reload": "Reload",
603			"msg.reservation": "Booking",
604			"msg.places": "Places",
605			"msg.purchases": "Purchases",
606			"msg.reset": "Reset",
607			"msg.free": "Free",
608			"msg.units": "Units",
609			"msg.currency": "Currency",
610			"msg.price": "Price",
611			"msg.discount": "Discount",
612			"msg.quantity": "Quantity",
613			"msg.checkout": "Checkout",
614			"msg.orders": "My orders",
615			"msg.order": "Order",
616			"msg.order.code": "Order code",
617			"msg.order.total": "Order summary",
618			"msg.order.data": "Order data",
619			"msg.order.items": "Order items",
620			"msg.order.new": "New order",
621			"msg.order.list": "Order list",
622			"msg.order.base": "Subtotal",
623			"msg.order.vat": "TAX",
624			"msg.order.add.discount": "Add discount",
625			"msg.clients": "Clients",
626			"msg.client": "Client",
627			"msg.operator": "Operator",
628			"msg.note": "Note",
629			"msg.tips": "Tip",
630			"msg.tips.allowed": "Tips allowed",
631			"msg.tips.add": "Add tip:",
632			"msg.anonymous.allowed": "Allow purchases without login",
633			"msg.cart.userRequired": "User required when add to cart",
634			"msg.customEntityOrder": "Custom entity order",
635			"msg.customEntityBooking": "Custom entity booking",
636			"msg.message" : "Message",
637		
638			"msg.print": "Print",
639			"msg.print.next" : "Print next",
640			"msg.pay": "Pay",
641			"msg.pay.mode": "Method of payment",
642			"msg.goToPay": "Go to checkout",
643			"msg.paid": "Payed",
644			"msg.paid.web": "Paid on the web",
645			"msg.delivered": "Delivered",
646			"msg.to_pay": "To pay",
647			"msg.to_deliver": "To deliver",
648			"msg.to_deliver_web": "To deliver web",
649			"msg.paymentMethods": "Payment methods",
650			"msg.paymentMethods.choose": "Choose payment method:",
651			"msg.paymentMethods.noAvailable": "There are no payment methods available",
652			"msg.payment.card.number": "Card number",
653			"msg.payment.card.expiration": "Expiration date (MM/YY)",
654			"msg.payment.card.control": "CVV/CVC",
655			"msg.payment.declined": "Payment declined",
656			"msg.payment.failed": "Payment not completed. The confirmation process was unsuccessful, so your payment was not processed. Please try again to complete your purchase. If the problem persists, please contact our support team.",
657			"msg.booking": "Booking",
658			"msg.bookings": "Bookings",
659			"msg.booking.title": "Make a booking",
660			"msg.book": "Book",
661			"msg.book.on": "Book on",
662			"msg.capacity": "Capacity",
663			"msg.max_bookings": "Max Bookings",
664			"msg.capacity_per_booking": "Capacity per Booking",
665			"msg.numPlaces": "Number of places",
666			"msg.booking.day": "Booking day",
667			"msg.booking.time": "Booking time",
668			"msg.booking.quantity": "Booking quantity",
669			"msg.booking.success": "Your booking has been successfully processed.",
670			"msg.booking.date.not.enough.places": "There are not enough free places. Choose another date/time.",
671			"msg.booking.status.confirmed": "Confirmed",
672			"msg.booking.status.pending_payment": "Pending payment",
673			"msg.booking.status.canceled": "Canceled",
674			"msg.booking.cancel": "Cancel booking",
675			"msg.booking.cancel.confirm.title": "Confirm booking cancellation",
676			"msg.booking.cancel.confirm.text": "Are you sure you want to cancel this booking? It cannot be undone.",
677			"msg.goto.user.bookings": "See my bookings",
678		
679			"msg.deliveryMethod": "Delivery method",
680			"msg.deliveryMethods": "Delivery methods",
681			"msg.deliveryMethods.choose": "Choose delivery method:",
682			"msg.delivery": "Delivery",
683			"msg.deliveryMethod.entity.tip": "Select a Form entity to request more delivery details or a Booking entity to link the purchase to a booking",
684			"msg.shippingCosts": "Shipping costs",
685		
686			"msg.item.entity.tip": "Select a Form entity to request more product details during checkout process",
687		
688			"msg.saved": "Saved",
689			"msg.created": "Created",
690			"msg.updated": "Updated",
691			"msg.sys.updated": "Updated",
692			"msg.removed": "Removed",
693			"msg.restored": "Restored",
694			"msg.moved": "Moved",
695			"msg.copied": "Copied",
696			"msg.done": "Done",
697			"msg.added": "Added",
698			"msg.verified": "Verified",
699		
700			"msg.yes": "Yes",
701			"msg.no": "No",
702			"msg.exists": "Exists",
703			"msg.exit": "Exit",
704			"msg.back": "Back",
705			"msg.next": "Next",
706			"msg.start": "Start",
707			"msg.end": "End",
708			"msg.finish": "Finish",
709			"msg.close": "Close",
710			"msg.accept": "Accept",
711			"msg.accept.all": "Accept all",
712			"msg.reject": "Reject",
713			"msg.cancel": "Cancel",
714			"msg.update": "Update",
715			"msg.send": "Send",
716			"msg.save": "Save",
717			"msg.saveAndBack": "Save & Back",
718			"msg.saveAll": "Save all",
719			"msg.showAll": "Show list",
720			"msg.remove": "Remove",
721			"msg.removeAll": "Remove results",
722			"msg.delete": "Delete",
723			"msg.copy": "Copy",
724			"msg.copy.from": "Copy from",
725			"msg.copy.to": "Copy to",
726			"msg.copy.content": "Duplicate content",
727			"msg.copy.system": "Duplicate system entities",
728			"msg.edit": "Edit",
729			"msg.add": "Add",
730			"msg.add.to": "Add to",
731			"msg.add.multiple": "Add multiple",
732			"msg.add.multiple.auto": "Add auto",
733			"msg.see": "See",
734			"msg.change": "Change",
735			"msg.search": "Search",
736			"msg.search.reset" : "Remove filters",
737			"msg.search.advanced": "Advanced search",
738			"msg.search.simple": "Simple search",
739			"msg.search.placeholder": "Search",
740			"msg.search.location": "Name, address or lat,lon",
741			"msg.create": "Create",
742			"msg.create_simple": "Create simple",
743			"msg.lazyformulas": "Lazy formulas",
744			"msg.modeler": "Modeler",
745			"msg.finalize": "Finalize",
746			"msg.upload": "Upload",
747			"msg.upload.preview": "Preview of first row",
748			"msg.upload.execute": "It's ok! Save all rows",
749			"msg.download": "Download",
750			"msg.download.stats": "Download Stats",
751			"msg.report": "Report",
752			"msg.reports": "Reports",
753			"msg.move": "Move",
754			"msg.clone": "Clone",
755			"msg.merge": "Merge",
756			"msg.new": "New",
757			"msg.document": "Document",
758			"msg.documents": "Documents",
759			"msg.notUsed": "Not used",
760			"msg.forbidden": "Forbidden",
761			"msg.go": "Go",
762			"msg.clear": "Clear",
763			"msg.logo": "Logo",
764			"msg.logo.footer": "Logo footer",
765			"msg.mode.test": "Test",
766			"msg.attach_documents": "Attach documents",
767			"msg.display_table": "Show list as table",
768			"msg.display_table_web": "Show list as table (web)",
769			"msg.normal": "Normal",
770			"msg.read" : "Read",
771			"msg.no_instance_required" : "No instance required",
772			"msg.no_user_required" : "No user required",
773			"msg.myaccount" : "Show in my account",
774			"msg.mycart" : "Show in my cart",
775			"msg.home_super" : "Show at super user home",
776			"msg.home_instance" : "Show in all instance homes",
777			"msg.auto_location" : "Auto location",
778			"msg.show_properties" : "Show properties",
779			"msg.show_key_values" : "Show key values",
780			"msg.duplicate_copy" : "Duplicate if copy",
781			"msg.one_item": "One item",
782			"msg.readonly": "Readonly",
783			"msg.hide_media": "Hide media",
784			"msg.security" : "Security",
785			"msg.options" : "Options",
786			"msg.tabs.hidden" : "Hide tabs",
787			"msg.change_status": "Change status",
788			"msg.workflow": "Workflow",
789			"msg.columnTimestamp": "Timestamp column",
790			"msg.columnStatus": "Status column",
791			"msg.calendar_date": "Date for Calendar",
792			"msg.resultType": "Result type",
793			"msg.app_object.desc" : "Entidad principal del modelado propio de un cliente (Los coordinadores solamente ven este tipo)",
794			"msg.listing.desc" : "Entidad secundaria del modelado propio de un cliente",
795			"msg.system.desc" : "Entidad de sistema. Ahi van, todos nuestrros syncronizers, notifiers etc",
796			"msg.order.desc" : "Path for default order. By default: app_object:priority:desc - schecule:by dates - otherwise:default column",
797			"msg.calendar_date.desc": "Path to the date column to be used in the calendar view, or the paths to the day and time columns separated by comma, or \"geo_schedule\" to show all generated dates. Default: sys.created",
798		
799			"msg.user.desc" : "Datos sensibles de usuario donde se guardará automáticamente el usuario en ‘geo_user’. Este tipo de entidades solo se podrán consultar desde el backend. Desde el frontend como mucho podremos dar acceso a su propio usuario para que lo vea y/o lo modifique.",
800			"msg.form.desc" : "Entidad de tipo formulario. Si además, es de tipo 'usuario' -> login required to access from frontend",
801			"msg.location.desc" : "De tipo ubicación",
802			"msg.geofence.desc" : "De tipo área de ubicación",
803			"msg.schedule.desc" : "De tipo 'evento'. Permite añadir fechas y/o programarlas",
804			"msg.trivia.desc" : "De tipo trivia",
805			"msg.prize.desc" : "De tipo premio",
806			"msg.buy.desc" : "De tipo producto para poder ser añadido al carrito de la compra",
807		
808			"msg.security.desc" : "Esta solo aplica para el frontend. Por default, la entidad es restrictiva al 100% por lo que si queremos que sea pública, tenemos que ir seleccionando los checks que necesitemos.",
809			"msg.read.desc" : "Permite lectura",
810			"msg.create.desc" : "Permite crear",
811			"msg.update.desc" : "Permite actualizar",
812			"msg.delete.desc" : "Permite borrar",
813			"msg.no_instance_required.desc" : "La instancia no es requerida",
814			"msg.no_user_required.desc" : "No necesita usuario logueado",
815			"msg.myaccount.desc" : "Mostrar entidad en mi cuenta",
816			"msg.mycart.desc" : "Mostrar entidad con datos extras a guardar en el carrito de la compra",
817		
818			"msg.create_simple.desc" : "Create with default column",
819			"msg.lazyformulas.desc": "No execute formulas when is list",
820			"msg.modeler.desc" : "Entity visible for modelers",
821			"msg.attach_documents.desc" : "Show attach documents at frontend",
822			"msg.show_properties.desc" : "Add extra properties at backend",
823			"msg.show_key_values.desc" : "Add extra key values at backend",
824			"msg.duplicate_copy.desc" : "Duplicate entity when copying",
825			"msg.one_item.desc" : "Entity only allows one item",
826			"msg.readonly.desc" : "Entity data is not updatable",
827			"msg.hide_media.desc" : "Hide media at backend",
828			"msg.display_table.desc" : "Show list as table",
829			"msg.display_table_web.desc" : "Show list as table (web)",
830			"msg.home_super.desc" : "Para indicar los que queremos que salgan al home de superusuario",
831			"msg.home_instance.desc" : "Sale por defecto en todas las instancias",
832			"msg.auto_location.desc" : "Llena automáticamente la ubicación en el momento de la creación del registro",
833		
834			"msg.section": "Section",
835			"msg.header": "Header",
836			"msg.footer": "Footer",
837			"msg.footer.text": "Footer text",
838			"msg.background": "Background",
839			"msg.secondary": "Secondary",
840			"msg.card": "Card",
841			"msg.template": "Template",
842			"msg.button": "Button",
843			"msg.config": "Config",
844			"msg.pages": "Pages",
845			"msg.page": "Page",
846			"msg.add.page": "New page",
847			"msg.add.item": "New item",
848			"msg.action": "Action",
849			"msg.none": "None",
850			"msg.instance.entity": "Entity/Instance",
851			"msg.default": "Default",
852			"msg.template1": "Template1",
853			"msg.fullImage": "Full image",
854			"msg.imageTexts": "Image and texts",
855			"msg.carousel": "Carousel",
856			"msg.gallery": "Gallery",
857			"msg.preview": "Preview",
858			"msg.classes": "Classes",
859			"msg.column": "Column",
860			"msg.left": "Left",
861			"msg.right": "Right",
862			"msg.imageText": "Text on image",
863			"msg.horizontal": "Horizontal",
864			"msg.enum.option": "Option",
865			"msg.restricted": "Restricted",
866		
867			"msg.select": "Select",
868			"msg.select.all": "Select all",
869			"msg.select.search": "Select or search",
870			"msg.select.media": "Select media",
871			"msg.select.target": "Select target",
872			"msg.select.entity": "Select entity",
873			"msg.select.instance": "Select instance",
874			"msg.select.instance.last": "Last selected",
875			"msg.select.from": "Select from",
876			"msg.select.addItem": "Add new item",
877		
878			"msg.new.copy": "Copy",
879			"msg.new.search": "Search",
880			"msg.new.multiple": "Multiple",
881			"msg.new.showCreated": "Show created",
882			"msg.new.hideCreated": "Hide created",
883			"msg.new.empty.extended": "Empty extended",
884			"msg.new.locations": "Search locations",
885			"msg.new.locations.auto": "Search locations auto",
886			"msg.new.add": "Add existing",
887			
888			"msg.new.version": "There is a new update available",
889			"msg.new.version.desc": "Would you like to update now?", 
890		
891			"msg.photos.add": "Add photos",
892			"msg.photos.google": "Google photos",
893			"msg.photos.wikipedia": "Wikipedia photos",
894		
895			"msg.folder": "Section",
896			"msg.my_smart": "Agenda",
897			"msg.new_smart": "Planner",
898		
899			"msg.property.type.category": "Section",
900			"msg.property.type.text": "Text",
901			"msg.property.type.link": "Link",
902			"msg.property.type.boolean": "Boolean",
903			"msg.property.type.number": "Number",
904			"msg.property.type.date": "Date",
905			"msg.property.type.color": "Color",
906			"msg.property.type.tags": "Tags",
907			"msg.property.type.list": "List",
908			"msg.property.type.asin": "ASIN",
909			"msg.property.type.hidden": "Hidden",
910		
911			"msg.column.type.text": "Translatable text",
912			"msg.column.type.boolean_int": "Boolean number",
913			"msg.column.type.entity": "Entity type",
914			"msg.column.type.list": "Entity list",
915			"msg.column.type.any": "Any object",
916			"msg.column.type.any_entity": "Any entity",
917			"msg.column.type.any_entity_list": "Any entity list",
918			"msg.column.type.columnList": "Column entity list",
919			"msg.column.type.columnEntity": "Column entity",
920			"msg.column.type.cron": "Cron expression",
921			"msg.column.type.enum": "Choose one",
922			"msg.column.type.enum_list": "Choose many",
923		
924			"msg.column.default": "Default",
925			"msg.column.hidden": "Hidden",
926			"msg.column.unexposed": "Unexposed",
927			"msg.column.lazyformula": "Lazy formula",
928			"msg.column.long": "Long",
929			"msg.column.html": "Html",
930			"msg.column.tab": "New tab",
931			"msg.column.calendar": "Calendar",
932			"msg.column.sort": "Sortable list",
933			"msg.column.updatable": "Updatable",
934			"msg.column.display": "Display",
935			"msg.column.nolabel": "No label",
936			"msg.column.required": "Required",
937			"msg.column.readonly": "Readonly",
938			"msg.column.search": "Search",
939			"msg.column.user": "User",
940			"msg.column.uppercase": "Uppercase",
941			"msg.column.autosave": "Autosave",
942			"msg.column.unique": "Unique",
943		
944			"msg.column.classes" : "Classes",
945			"msg.column.size": "Size",
946			"msg.column.default_value": "Default value",
947			"msg.column.number": "Number",
948			"msg.column.min": "Min",
949			"msg.column.max": "Max",
950			"msg.column.maxlength": "Max length",
951			"msg.column.minlength": "Min length",
952			"msg.column.pattern": "Pattern",
953			"msg.column.super": "Super",
954			"msg.column.admin": "Admin",
955			"msg.column.admincreate": "Admin create",
956			"msg.column.condition": "Condition (javascript)",
957			"msg.column.delimiter": "Delimiter (or row-break)",
958			"msg.column.buttonStyle": "Button style",
959
960			"msg.column.default.desc": "Default column (only one permitted)",
961			"msg.column.hidden.desc": "Hidden column at frontend",
962			"msg.column.unexposed.desc": "Hidden column also in backend",
963			"msg.column.lazyformula.desc": "No execute the formula when is list",
964			"msg.column.long.desc": "Column full width (size:12)",
965			"msg.column.html.desc": "Text is html",
966			"msg.column.tab.desc": "Show column in a new tab",
967			"msg.column.calendar.desc": "Show listed data in a calendar",
968			"msg.column.sort.desc": "Permite ordenar si la columna es de tipo list",
969			"msg.column.updatable.desc": "Columna modificable en frontend",
970			"msg.column.autosave.desc": "Save form automatically when this column was modified (it applies only in web forms)",
971			"msg.column.unique.desc": "Validate column unique",
972			"msg.column.display.desc": "Los campos que quieres que se desplieguen en las listas",
973			"msg.column.nolabel.desc": "Oculta nombre de columna (solo para frontend)",
974			"msg.column.required.desc": "Columna requerida en formas",
975			"msg.column.readonly.desc": "La columna se muestra en modo lectura si hay valor",
976			"msg.column.search.desc": "Aparece en la sección de búsqueda (tambien sirve para las ordenaciones)",
977			"msg.column.user.desc": "Campo actualizable de reservas/carrito",
978			"msg.column.uppercase.desc": "Guarda el valor en mayusculas",
979		
980			"msg.column.size.desc": "Tamaño de la columna. Default:4, max:12",
981			"msg.column.default_value.desc": "Para indicar un valor por defecto cuando se crea y no tiene valor",
982			"msg.column.min.desc": "Validación de valor mínimo numérico",
983			"msg.column.max.desc": "Validación de valor máximo numérico",
984			"msg.column.maxlength.desc": "Validación de longitud mínima",
985			"msg.column.minlength.desc": "Validación de longitud máxima",
986			"msg.column.pattern.desc": "Validación con expressión regular",
987			"msg.column.condition.desc": "Se mostrará el campo sólo si se cumple la condición (javascript). Ej.: obj.name == 'Alice'",
988		
989			"msg.group": "Group",
990			"msg.system": "System",
991			"msg.configuration": "Configuration",
992			"msg.myEntities": "My entities",
993			"msg.lists": "Listings",
994		
995			"msg.preferences": "Info",
996			"msg.preferences.powered": "Powered by GEO®",
997
998			"msg.number.decimal.separator": "point",
999			"msg.currency.decimal.separator": "and",
1000			"msg.currency.USD.decimals": "cents",
1001			"msg.currency.EUR.decimals": "cent",
1002			"msg.currency.MXN.decimals": "centavos",
1003
1004			"msg.trigger": "Trigger",
1005			"msg.trigger.contextEntity": "Context Entity",
1006			"msg.trigger.contextEntity.or": "or",
1007			"msg.trigger.event": "Event",
1008			"msg.trigger.event.add": "Add starting event",
1009			"msg.trigger.event.ctx.notSame": "All events must apply at same entity",
1010			"msg.trigger.event.condition.notFormula": "Condition is not a formula",
1011			"msg.trigger.event.column.empty": "Column is empty",
1012			"msg.trigger.event.time.empty": "Time is empty",
1013			"msg.trigger.event.scheduler.empty": "Scheduler is not defined",
1014			"msg.trigger.action": "Action",
1015			"msg.trigger.action.add": "Add action to do",
1016			"msg.trigger.action.notifier.empty": "No notifier selected",
1017			"msg.trigger.action.notifier.notFound": "Notifier selected not found",
1018			"msg.trigger.action.notifier.disabled": "Notifier selected is disabled",
1019			"msg.trigger.action.synchronizer.empty": "No synchronizer selected",
1020			"msg.trigger.action.synchronizer.notFound": "Synchronizer selected not found",
1021			"msg.trigger.action.synchronizer.disabled": "Synchronizer selected is disabled",
1022			"msg.trigger.action.spreadsheet.empty": "No synchronizer selected",
1023			"msg.trigger.action.spreadsheet.notFound": "Spreadsheet selected not found",
1024			"msg.trigger.action.spreadsheet.disabled": "Spreadsheet selected is disabled",
1025			"msg.trigger.action.entity.empty": "No entity selected",
1026			"msg.trigger.action.entity.notFound": "Entity selected not found",
1027			"msg.trigger.action.entity.disabled": "Entity selected is disabled",
1028			"msg.trigger.action.value.empty": "Value is empty",
1029			"msg.trigger.action.path.empty": "Path is empty",
1030			"msg.trigger.action.set_to.empty": "It's mandatory to indicate where to set value",
1031			"msg.trigger.action.custom_function.empty": "It's mandatory to indicate function to execute",
1032
1033			// LOGIN
1034			"msg.login": "Sign in",
1035			"msg.login.google": "Sign in with Google",
1036			"msg.login.register": "No account yet?",
1037			"msg.login.resetPwd": "I forgot my password",
1038			"msg.login.error": "Invalid credentials",
1039			"msg.register": "Sign up",
1040			"msg.register.update": "Account update",
1041			"msg.register.update.desc": "Please, indicate your email and review the Terms of Service and Licenses",
1042			"msg.register.exists": "This account already exists. Please, try to restore your password.",
1043			"msg.register.exists.validate": "This account already exists.",
1044			"msg.restore": "Restore",
1045			"msg.restore.title": "Create new password",
1046			"msg.restore.body" : "Hello,<br><br>Click this link to set your account password.<br><br><a href='{0}'>Create password</a><br><br>If you did not request this change, please ignore this email.<br><br>Thank you",
1047			"msg.restore.sent1": "Please check your email.",
1048			"msg.restore.sent2": "You will have received an email from WE with a link where you can indicate your password.",
1049			"msg.restore.sent3": "If you do not receive the email, check that the email is correct or wait 5 minutes please.",
1050			"msg.verify.title": "Account not verified",
1051			"msg.verify.desc": "Check your email to finish the verification of your account please",
1052			"msg.verify.error": "The account is not yet verified",
1053			"msg.verify.btn": "Done",
1054			"msg.verify": "Verify account",
1055			"msg.verify.body" : "Hello,<br><br>Click this link to verify your account.<br><br><a href='{0}'>Verify account</a><br><br>If you did not request this change, please ignore this email.<br><br>Thank you",
1056			"msg.disabled.register": "Sign up not allowed",
1057			"msg.password.new.invalid": "The new password is invalid",
1058			"msg.licenses.accept": "I have read and accept the Terms of Service and Licenses",
1059			"msg.logout": "Sign out",
1060			"msg.mslogin" : "MS Login",
1061			
1062			// CALENDAR
1063			"msg.calendar.default" : "Default calendar",
1064			"msg.calendar.timetable" : "Timetable",
1065			"msg.calendar.holidays" : "Holidays",
1066			"msg.calendar.holiday.set" : "Holiday",
1067			"msg.calendar.am": "AM", 
1068			"msg.calendar.pm": "PM", 
1069			"msg.calendar.time": "Time", 
1070			"msg.calendar.week": "Day",
1071			"msg.calendar.drag": "Drag to select hours", 
1072			"msg.calendar.reset": "Reset",
1073			"msg.calendar.monday":"Monday", 
1074			"msg.calendar.tuesday":"Tuesday", 
1075			"msg.calendar.wednesday":"Wednesday", 
1076			"msg.calendar.thursday":"Thursday", 
1077			"msg.calendar.friday":"Friday", 
1078			"msg.calendar.saturday":"Saturday", 
1079			"msg.calendar.sunday":"Sunday",
1080		
1081			// CONFIRM
1082			"msg.confirm": "Are you sure you want to perform this operation?",
1083			"msg.confirm.removeInstance": "<span style='color:red'>Warning!</span> <b>This is going to remove all related data.</b>",
1084			"msg.confirm.removeAll": "<span style='color:red'>Warning!</span> <b>This is going to remove all results.</b>",
1085			"msg.confirm.leave": "Are you sure you want to leave this page?",
1086		
1087			"msg.remove.warns.title": "Warning!",
1088			"msg.remove.warns.to_remove": "This will delete:",
1089			"msg.remove.warns.in_use": "This object still has relationships with:",
1090		
1091			// COOKIES
1092			"msg.cookies.title": "Cookies policy",
1093			"msg.cookies.description": "<p>We use cookies in order to offer you a correct use of the site and our services.</p><p>You can get more information in our <a onclick='$angular.openLicense(\"cookies\", false)'>Cookies Policy</a>.</p><p>Press the ACCEPT button, to confirm that you have read and accepted the information.</p>",
1094		
1095			// TERMS
1096			"msg.terms.title": "Review our Terms of Service and Licenses",
1097			"msg.terms.description": "By continuing to use this service your are agreeing to these terms.",
1098			"msg.terms.cookies": "Cookies policy",
1099			"msg.terms.eula": "EULA",
1100			"msg.terms.privacy": "Privacy Policy",
1101			"msg.terms.terms": "Terms of Service",
1102			"msg.terms.platform": "Platform Terms of Service Fidesur",
1103		
1104			// FORM VALIDATIONS
1105			"required": "This field is required.",
1106			"fields.required": "Fields required.",
1107			"email.required": "Email required",
1108		
1109			// SERVER MESSAGES
1110			"server.alreadyExists": "already exists",
1111			"server.entityExists": "Id '{0}' already exists",
1112			"server.entityNotExists": "Id '{1}' does not exist in '{0}'",
1113			"server.entity.allColumnsRequired": "All columns fields are required",
1114			"server.entity.incorrectColumn": "Incorrect column {0}",
1115			"server.entity.repeatedColumn": "Repeated column {0}",
1116			"server.geoFence.invalid": "Invalid Area",
1117			"server.geo.id.required": "Id required",
1118			"server.geo.customId.invalid": "Invalid custom id '{0}'",
1119			"server.geo.hasData": "This entity has data in '{0}'",
1120			"server.geo.used": "This is used in '{0}' '{1}'",
1121			"server.geo.noInstance": "No instance selected",
1122			"server.geo.name.required": "Name is required",
1123			"server.geo.instanceBelongItself": "Instance cannot belong to itself",
1124			"server.geo.instanceContainsItself": "Instance cannot contains itself",
1125			"server.media.typeRequired": "MediaType is required",
1126			"server.media.noSourceLink": "No source link provided to create the media",
1127			"server.media.sourceLink.incorrect": "Source link incorrect",
1128			"server.media.sourceLink.noData": "No data for source link",
1129			"server.schedule.timeFrom.invalid": "Time from is invalid. Format HH:mm",
1130			"server.schedule.timeTo.invalid": "Time to is invalid. Format HH:mm",
1131			"server.schedule.date.required": "Date cannot be empty",
1132			"server.schedule.cron.invalid": "Cron expression invalid: {0}",
1133			"server.noEntities": "No entities defined",
1134			"server.noEntities.found": "No entities found",
1135			"server.words.required": "Search words are required",
1136			"server.area.required": "Area is required",
1137			"server.noPlacesFound": "No places found",
1138			"server.wikiPage.required": "Wikipedia page is required",
1139			"server.smarts.disabled": "Smarts disabled",
1140			"server.spreadsheet.empty": "Spreadsheet has no rows",
1141
1142			// APP MESSAGES
1143			"msg.app.title": "GEO",
1144			"msg.app.description": "GEO",
1145		
1146			"msg.home": "Home",
1147			"msg.settings": "Settings",
1148		
1149			"msg.instances": "Instances",
1150			"msg.instance": "Instance",
1151			"msg.subInstance": "SubInstance",
1152			"msg.belongsTo": "Belongs to",
1153			"msg.contains": "Contains",
1154			"msg.instance.home": "Home",
1155			"msg.instance.showCurrent": "Show current instance",
1156			"msg.type.add": "Add type",
1157			"msg.entities": "Entities",
1158			"msg.entity": "Entity",
1159			"msg.entity.name": "Entity name",
1160			"msg.entity.type": "Entity type",
1161			"msg.entity.value": "Entity value",
1162			"msg.entity.filter.unused": "Unused non-system entities",
1163			"msg.users": "Users",
1164			"msg.user": "User",
1165			"msg.user.account": "My account",
1166			"msg.user.qr": "My QR code",
1167			"msg.user.name": "User name",
1168			"msg.menu": "Menu",
1169			"msg.target": "Target",
1170			"msg.recents": "Recents",
1171			"msg.generateQR": "View QR code",
1172		
1173			"msg.translations": "Translations",
1174			"msg.translations.allowedVariables": "This text allows variables",
1175			"msg.translations.variables": "variables",
1176			"msg.translations.variables.info.text": "In this text it's allowed to add variables that will be resolved with the value they have at the time of displaying the text.",
1177			"msg.translations.variables.info.list": "Enabled variables:",
1178			"msg.translations.variables.info.sample.title": "How to use variables",
1179			"msg.translations.variables.info.sample.text": "Each variable has assigned a number. In the part of the text that we want the variable to be written, we put the number between braces. Example:",
1180			"msg.translations.variables.info.sample.original.label": "We save the text:",
1181			"msg.translations.variables.info.sample.original.value": "Hi {0}! You have visited {1} countries!",
1182			"msg.translations.variables.info.sample.result.label": "It's resolved:",
1183			"msg.translations.variables.info.sample.result.value": "Hi John! You have visited 15 countries!",
1184			"msg.translations.media.description": "It allows to overwrite the media file by another file for an specific language.",
1185		
1186			"msg.general": "General",
1187			"msg.sourceEntity": "Source entity",
1188			"msg.resultEntities": "Entities",
1189			"msg.use": "Use",
1190			"msg.useTypes": "Use types",
1191			"msg.usePriority": "Use priority",
1192			"msg.useProximity": "Use proximity",
1193			"msg.usePeople": "Use people",
1194			"msg.useDates": "Use dates",
1195			"msg.useLocation": "Use location",
1196			"msg.useLocationInitial": "Use initial location",
1197			"msg.useCategories": "Use categories",
1198			"msg.useTranslations": "Use translations",
1199			"msg.dateFrom": "Date from",
1200			"msg.dateTo": "Date to",
1201			"msg.until": "Until",
1202			"msg.timeFrom": "Time from",
1203			"msg.timeTo": "Time to",
1204			"msg.people": "People",
1205			"msg.senior": "Senior",
1206			"msg.adult": "Adult",
1207			"msg.teen": "Teen",
1208			"msg.kid": "Kid",
1209			"msg.dates": "Dates",
1210			"msg.types": "Types",
1211			"msg.first_time": "First time",
1212			"msg.been_here": "Been here",
1213			"msg.live_here": "Live here",
1214			"msg.numResults": "Number of results",
1215			"msg.maxResults": "Max results",
1216			"msg.defaultTimeBetween": "Time between items",
1217			"msg.useAverageTime": "Use average time",
1218			"msg.defaultAverageTime": "Default duration",
1219			"msg.includeRestaurants": "Include restaurants",
1220			"msg.useInitialPosition": "Use initial position",
1221			"msg.maxDays": "Max days",
1222			"msg.defaultMaxDistance": "Default max distance",
1223			"msg.maxDistance": "Max distance",
1224			"msg.walking": "Walking",
1225			"msg.driving": "Driving",
1226			"msg.zoom": "Zoom",
1227			"msg.countryCode": "Country code",
1228			"msg.timezone": "Time zone",
1229			"msg.doNotPlan": "Do not plan",
1230		
1231			"msg.averageTime": "Average time",
1232			"msg.outdoor": "OutDoor",
1233		
1234			"msg.info": "Info",
1235			"msg.location": "Location",
1236			"msg.locationList": "Locations",
1237			"msg.list": "List",
1238			"msg.map": "Map",
1239			"msg.calendar": "Calendar",
1240			"msg.trivia": "Trivia",
1241			"msg.prize": "Prize",
1242			"msg.form": "Form",
1243			"msg.buy": "Buy",
1244			"msg.buy.verb": "Buy",
1245			"msg.buyer": "Buyer",
1246			"msg.identity": "Identity",
1247			"msg.schedule": "Schedule",
1248			"msg.schedules": "Schedules",
1249			"msg.properties": "Properties",
1250			"msg.media": "Media",
1251			"msg.media.add": "Add media",
1252			"msg.relations": "Relations",
1253			"msg.smarter": "Smarts",
1254			"msg.smartRate": "Rating",
1255		
1256			"msg.latitude": "Latitude",
1257			"msg.longitude": "Longitude",
1258			"msg.mobile": "Mobile",
1259			"msg.indoorLevel": "Indoor Level",
1260			"msg.extraInfo": "Extra Info",
1261			"msg.googlePlaceId": "Google Place ID",
1262			"msg.timetables": "Timetables",
1263			"msg.timetable.invalidHours": "Invalid hours",
1264			"msg.website": "Website",
1265		
1266			"msg.image": "Image",
1267			"msg.audio": "Audio",
1268			"msg.video": "Video",
1269			"msg.others": "Others",
1270			"msg.images": "Images",
1271			"msg.image.byUrl": "From url",
1272			"msg.licenses": "Licenses",
1273			"msg.license": "License",
1274			"msg.author": "Author",
1275			"msg.sourceLink": "Source link",
1276			"msg.sourceMediaUrl": "Source media url",
1277			"msg.dropzone.drop": "Drag and drop files here or <label class='btn-link'>click to select</label>",
1278			"msg.tags.placeholder": "Add tags",
1279			"msg.wikipedia": "Wikipedia",
1280			"msg.wikipedia.url": "Wikipedia url",
1281			"msg.wikipedia.search": "Search in Wikipedia",
1282			"msg.wikiPage": "Wikipedia page",
1283			"msg.google": "Google",
1284			"msg.flickr": "Flickr",
1285			"msg.seeOriginal": "See original",
1286			"msg.seeBig": "See big",
1287			"msg.seeResized": "See resized",
1288			"msg.seeThumb": "See thumb",
1289			"msg.seeOnMap": "See on map",
1290		
1291			"msg.schedule.allDay": "All day",
1292		
1293			"msg.searchLocation": "Search single point",
1294			"msg.searchLocations": "Search multiple points",
1295			"msg.geofence": "Area",
1296			"msg.geofence.select": "Select area",
1297			"msg.geofence.search": "Search or draw area",
1298			"msg.geofence.radius": "Set area by radius",
1299			"msg.geofence.draw": "Draw",
1300			"msg.geofence.drawMinimum": "Set 3 points minimum",
1301		
1302			"msg.locationList.dragToSort": "Drag to sort",
1303			"msg.locationList.routeUseFirstLocations": "Route only use 10 first locations",
1304			"msg.locationList.calculateRoute": "Calculate route",
1305			"msg.locationList.calculateRouteSort": "Calculate route and sort",
1306			"msg.locationList.calculateRoute.walking": "Walking",
1307			"msg.locationList.calculateRoute.driving": "Driving",
1308		
1309			"msg.columns": "Columns",
1310			"msg.columns.add": "Add column",
1311			"msg.columns.addEntity": "Add entity",
1312			"msg.columns.default": "Default columns",
1313			"msg.entity.inner": "Inner entities",
1314			"msg.instance.add": "Add new instance",
1315		
1316			"msg.question": "Question",
1317			"msg.answers": "Answers",
1318			"msg.answer.correct": "Correct",
1319			"msg.answerExplanation": "Explanation",
1320			"msg.answers.is_survey": "Is survey",
1321		
1322			"msg.prize.image_inactive": "Image inactive",
1323			"msg.prize.image_active": "Image active",
1324		
1325			"msg.data": "Data",
1326			"msg.google.data": "Google data",
1327			"msg.wiki.data": "Wikipedia data",
1328			"msg.place.exists": "This place already exists",
1329		
1330			"status.pending": "Pending review",
1331			"status.working": "Working",
1332			"status.resolved": "Resolved",
1333			"status.accepted": "Accepted",
1334			"status.rejected": "Rejected",
1335		
1336			"day.sunday": "Sunday",
1337			"day.monday": "Monday",
1338			"day.tuesday": "Tuesday",
1339			"day.wednesday": "Wednesday",
1340			"day.thursday": "Thursday",
1341			"day.friday": "Friday",
1342			"day.saturday": "Saturday",
1343		
1344			"msg.time.format": "Time HH:mm",
1345			"msg.distance.kms": "Distance Kms",
1346		
1347			"msg.move.desc": "This operation move all items of current instance to the target entity.",
1348			"msg.similars.found": "Similiars found!",
1349			"msg.similars.found2": "Similar entities found",
1350			"msg.similars.found3": "Would you still like to create it?",
1351		
1352			"msg.nearbyUsers.title": "Proximity with other users",
1353			"msg.nearbyUsers.distance": "Max distance (in meters)",
1354			"msg.nearbyUsers.time": "Max time difference (in minutes)",
1355		
1356			"msg.tax.includedInPrice": "Prices with taxes included",
1357			"msg.tax.vat": "Tax",
1358			"msg.tax.vat.percent": "Tax (%)",
1359		
1360			"msg.email.header": "Please, DO NOT RESPOND TO THIS EMAIL.\nThis message has been generated automatically.",
1361			"msg.email.footer": "This message is intended exclusively for its recipient and may contain privileged or confidential information. If you are not the intended recipient, you are notified that unauthorized use, disclosure and/or copying is prohibited under current legislation. If you have received this message by mistake, we ask you to notify us immediately by this 
1361same means and proceed to its destruction.",
1362		
1363			"msg.scheduler.title": "Schedule this event",
1364			"msg.scheduler": "Scheduler",
1365			"msg.recurrent": "Recurrent",
1366			"msg.allDay": "All day",
1367			"msg.useDuration" : "Use duration",
1368			"msg.day.from": "Starting date",
1369			"msg.day.to": "Ending date",
1370			"msg.starting": "Starting",
1371			"msg.ending": "Ending",
1372			"msg.duration": "Duration",
1373			"msg.blockDates": "Block dates",
1374			"msg.blockDates.description": "You won't be able to schedule events on blocked dates",
1375			"msg.blockDays": "Block days",
1376			"msg.blockHours": "Block hours",
1377			"msg.generatedDates": "Generated dates",
1378			"msg.endDay": "End day",
1379			"msg.endTime": "End time",
1380			"msg.repeat": "Repeat",
1381			"msg.minutes": "Minutes",
1382			"msg.newDate": "New date",
1383		
1384			"msg.timeAndAttendance": "Time and Attendance",
1385			"msg.worked": "Worked",
1386			"msg.workedHours": "Worked hours",
1387			"msg.extraHours": "Extra hours",
1388			"msg.attendantsEntity": "Attendants entity",
1389			"msg.workdaysEntity": "Workdays entity",
1390			"msg.accessRecordsEntities": "Access records entities",
1391			"msg.attendantColumn": "Attendant column",
1392			"msg.dateColumn": "Date column",
1393			"msg.numFutureWorkdays": "Num future workdays",
1394			"msg.access.type": "Access type",
1395			"msg.access.planned": "Planned time",
1396			"msg.access.record": "Record time",
1397			"msg.difference": "Difference",
1398			"msg.access.type.in": "In",
1399			"msg.access.type.out": "Out",
1400			"msg.access.status.registered": "Registered",
1401			"msg.access.status.unregistered": "Unregistered",
1402			"msg.access.status.unplanned": "Unplanned",
1403		
1404			"msg.action.login": "Login",
1405			"msg.action.logout": "Logout",
1406			"msg.action.licenses": "Accept licenses",
1407			"msg.action.download": "Download",
1408			"msg.action.location": "Locate",
1409			"msg.action.prize": "Win prize",
1410			"msg.action.qr": "Read QR",
1411			"msg.action.details": "View details",
1412			"msg.action.favorite": "Mark favorite",
1413			"msg.action.checkin": "Check in",
1414			"msg.action.rate": "Rate",
1415			"msg.action.comment": "Comment",
1416			"msg.action.play_audio": "Play audio",
1417			"msg.action.play_video": "Play video",
1418			"msg.action.share_media": "Share media",
1419			"msg.action.smart": "Smart system",
1420			"msg.action.smart_create": "Smart system: Create",
1421			"msg.action.smart_add": "Smart system: Add",
1422			"msg.action.smart_remove": "Smart system: Remove",
1423			"msg.action.smart_move": "Smart system: Move",
1424			"msg.action.smart_move_to_day": "Smart system: Move to day",
1425			"msg.action.trivia_answer": "Trivia: Answer question",
1426			"msg.action.trivia_results": "Trivia: View results",
1427			"msg.action.survey": "Fill survey",
1428			"msg.action.book": "Book",
1429			"msg.action.buy": "Purchase order",
1430		
1431			"msg.safeemployee": "Safe Employee",
1432		
1433			"msg.scheduler.minutely": "Minutes",
1434			"msg.scheduler.hourly": "Hours",
1435			"msg.scheduler.daily": "Daily",
1436			"msg.scheduler.weekly": "Weekly",
1437			"msg.scheduler.monthly": "Monthly",
1438			"msg.scheduler.yearly": "Yearly",
1439			"msg.scheduler.advanced": "Advanced",
1440			"msg.scheduler.every": "Every",
1441			"msg.scheduler.on": "on",
1442			"msg.scheduler.onThe": "On the",
1443			"msg.scheduler.at": "at",
1444			"msg.scheduler.of": "of",
1445			"msg.scheduler.and": "and",
1446			"msg.scheduler.second": "second",
1447			"msg.scheduler.seconds": "second(s)",
1448			"msg.scheduler.minute": "minute",
1449			"msg.scheduler.minutes": "minute(s)",
1450			"msg.scheduler.hour": "hour",
1451			"msg.scheduler.hours": "hour(s)",
1452			"msg.scheduler.day": "day",
1453			"msg.scheduler.days": "day(s)",
1454			"msg.scheduler.month": "month",
1455			"msg.scheduler.months": "months(s)",
1456			"msg.scheduler.weekDay": "week day (Monday through Friday)",
1457			"msg.scheduler.dayOfWeek.MON": "Monday",
1458			"msg.scheduler.dayOfWeek.TUE": "Tuesday",
1459			"msg.scheduler.dayOfWeek.WED": "Wednesday",
1460			"msg.scheduler.dayOfWeek.THU": "Thursday",
1461			"msg.scheduler.dayOfWeek.FRI": "Friday",
1462			"msg.scheduler.dayOfWeek.SAT": "Saturday",
1463			"msg.scheduler.dayOfWeek.SUN": "Sunday",
1464			"msg.scheduler.cronExpression": "Cron expression",
1465			"msg.scheduler.cronExpressionDetails": "More details about how to create these expressions can be found",
1466			"msg.scheduler.here": "here",
1467			"msg.scheduler.monthDay.1W": "First weekday",
1468			"msg.scheduler.monthDay.LW": "Last weekday",
1469			"msg.scheduler.monthDay.L": "Last day",
1470			"msg.scheduler.monthWeek.#1": "First",
1471			"msg.scheduler.monthWeek.#2": "Second",
1472			"msg.scheduler.monthWeek.#3": "Third",
1473			"msg.scheduler.monthWeek.#4": "Fourth",
1474			"msg.scheduler.monthWeek.#5": "Fifth",
1475			"msg.scheduler.monthWeek.L": "Last",
1476			"msg.scheduler.month.1": "January",
1477			"msg.scheduler.month.2": "February",
1478			"msg.scheduler.month.3": "March",
1479			"msg.scheduler.month.4": "April",
1480			"msg.scheduler.month.5": "May",
1481			"msg.scheduler.month.6": "June",
1482			"msg.scheduler.month.7": "July",
1483			"msg.scheduler.month.8": "August",
1484			"msg.scheduler.month.9": "September",
1485			"msg.scheduler.month.10": "October",
1486			"msg.scheduler.month.11": "November",
1487			"msg.scheduler.month.12": "December",
1488		
1489			// FRONT END MESSAGES
1490			"msg.web.whatDo": "What to do?",
1491			"msg.web.know": "Know",
1492			"msg.web.knowMore": "Know more",
1493			"msg.web.destinations": "Destinations",
1494			"msg.web.seeAll": "See all",
1495			"msg.web.seeMore": "See more",
1496			"msg.web.select": "Where to go?",
1497			"msg.web.recommendations": "Do you want personalized recommendations?",
1498			"msg.web.discover": "Discover our destinations",
1499			"msg.web.filters": "Filters",
1500		
1501			"msg.web.cart": "Shopping cart",
1502			"msg.web.cart.empty": "Shopping cart empty",
1503			"msg.web.cart.checkout": "Checkout",
1504			"msg.web.cart.validateCard": "Validate card",
1505			"msg.web.cart.finish": "Finish payment",
1506			"msg.web.cart.continue": "Continue buying",
1507			"msg.web.cart.done.desc": "Your order has been successfully processed.",
1508			"msg.web.cart.contact": "Contact",
1509			"msg.web.cart.identity.guest": "Checkout as guest",
1510			"msg.web.cart.identity.user": "Checkout with account",
1511
1512			"msg.web.plan": "Create agenda",
1513			"msg.web.plan.new": "New",
1514			"msg.web.plan.people": "Who travels?",
1515			"msg.web.plan.types": "Did you visit previously?",
1516			"msg.web.plan.location": "Location",
1517			"msg.web.plan.categories": "Preferences",
1518			"msg.web.plan.dates": "Dates",
1519			"msg.web.plan.results": "See results",
1520		
1521			"msg.web.agenda": "My Agenda",
1522			"msg.web.agenda.open": "Open",
1523			"msg.web.agenda.duration": "Duration",
1524			"msg.web.agenda.noResults": "No data found",
1525		
1526			"msg.web.footer": "Powered by GEO®"
1527		},
1528		
1529		"es" : {
1530			// GENERIC MESSAGES
1531			"msg.error": "¡Oops! Algo salió mal.",
1532			"msg.error.noConnection": "Sin conexión a Internet",
1533			"msg.error.502" : "El servidor no está disponible. Por favor, vuelva a intentarlo en un momento.",
1534			"msg.notFound": "Recurso no encontrado",
1535			"msg.accessDenied": "Usted no tiene permisos para acceder a este recurso.",
1536			"msg.tooManyAttempts": "Demasiados intentos. Por favor, espere un momento y vuelve a intentarlo.",
1537			"msg.data.conflict": "Por favor, actualice la página debido a un conflicto de datos.",
1538			"msg.data.invalid" : "Hay datos incorrectos. Por favor, revise sus datos.",
1539			"msg.role.notAllowed" : "Rol no permitido",
1540			"msg.invalid": "Inválido",
1541			"msg.noDataFound" : "No se han encontrado resultados",
1542		
1543			"msg.language": "Idioma",
1544			"msg.lang.en": "Inglés",
1545			"msg.lang.es": "Español",
1546			"msg.lang.fr": "Francés",
1547		
1548			"role.super": "Super usuario",
1549			"role.admin": "Administrador",
1550			"role.coordinator": "Coordinador",
1551			"role.basic": "Usuario asociado",
1552			
1553			"msg.cybersource" : "Tarjeta de crédito/débito",
1554		
1555			"msg.title": "Título",
1556			"msg.subtitle": "Subtítulo",
1557			"msg.status": "Estado",
1558			"msg.name": "Nombre",
1559			"msg.name.extra": "Sufijo",
1560			"msg.code": "Código",
1561			"msg.value": "Valor",
1562			"msg.text": "Texto",
1563			"msg.type": "Tipo",
1564			"msg.color": "Color",
1565			"msg.priority": "Prioridad",
1566			"msg.email": "Email",
1567			"msg.phone": "Teléfono",
1568			"msg.phoneInt": "Teléfono internacional",
1569			"msg.description": "Descripción",
1570			"msg.comments": "Comentarios",
1571			"msg.address": "Dirección",
1572			"msg.hint": "Pista",
1573			"msg.userName": "Nombre de usuario",
1574			"msg.usernameOrEmail": "Nombre de usuario o email",
1575			"msg.password": "Contraseña",
1576			"msg.password.new": "Contraseña nueva",
1577			"msg.sortBy": "Ordenar por",
1578			"msg.enabled": "Habilitado",
1579			"msg.disabled": "Deshabilitado",
1580			"msg.tags": "Tags",
1581			"msg.category": "Categoría",
1582			"msg.categories": "Categorías",
1583			"msg.category.types": "Categorías por entidad",
1584			"msg.category.parents": "Padres",
1585			"msg.category.children": "Hijos",
1586			"msg.category.notExisting": "No existentes",
1587			"msg.date": "Fecha",
1588			"msg.datetime": "Fecha y hora",
1589			"msg.time": "Hora",
1590			"msg.day": "Día",
1591			"msg.empty": "Vacío",
1592			"msg.keywords": "Palabras clave",
1593			"msg.radius": "Radio",
1594			"msg.customId": "Código personalizado",
1595			"msg.customId.placeholder": "Utilice un código auto descriptivo (ej: primeras letras)",
1596			"msg.role": "Rol",
1597			"msg.morning": "Mañana",
1598			"msg.afternoon": "Tarde",
1599			"msg.night": "Noche",
1600			"msg.skipped": "Omitidos",
1601			"msg.behavior": "Comportamiento",
1602			"msg.total": "Total",
1603			"msg.subtotal": "Subtotal",
1604			"msg.fixed": "Fijo",
1605			"msg.url": "Url",
1606			"msg.elements": "elementos",
1607			"msg.dashboard": "Dashboard",
1608			"msg.spreadsheet": "Spreadsheet",
1609			"msg.recovered": "Recuperados",
1610			"msg.inRisk": "En Riesgo",
1611			"msg.sick": "Enfermos",
1612			"msg.suspicious": "Sospechosos",
1613			"msg.actions": "Acciones",
1614			"msg.products": "Productos",
1615			"msg.employee": "Empleado",
1616			"msg.inheritance": "Herencia",
1617			"msg.plusDays": "Sumar días",
1618			"msg.more" : "Más",
1619			"msg.less" : "Menos",
1620			"msg.reloadInSeconds": "Recargar en {{seconds}} segundos",
1621			"msg.showMoreOptions": "mostrar más opciones",
1622			"msg.alignment" : "Alineación",
1623			
1624			"msg.operator.eq": "es igual que",
1625			"msg.operator.ne": "no es igual que",
1626			"msg.operator.gt": "es mayor que",
1627			"msg.operator.gte": "es mayor o igual que",
1628			"msg.operator.lt": "es menor que",
1629			"msg.operator.lte": "es menor o igual que",
1630			
1631			"msg.dashboard.map.title": "Mapa de casos de COVID-19",
1632			"msg.dashboard.map.description": "Se muestran las ubicaciones de casos confirmados",
1633			"msg.dashboard.graph.title": "Gráfica de casos de COVID-19",
1634			"msg.dashboard.graph.description": "Se muestran los casos acumulados diarios",
1635			"msg.datamixer.error.tooMuchTime": "Los cálculos toman demasiado tiempo. Contactar con el departamento de sistemas para mejorar los procesos.",
1636		
1637			"msg.all": "Todos",
1638			"msg.show": "Mostrar",
1639			"msg.required": "Requerido",
1640			"msg.app_object": "Objecto de App",
1641			"msg.listing": "Listado",
1642			"msg.link": "Vínculo",
1643			"msg.noPlan": "No planificar",
1644			"msg.noShadow": "Sin sombra",
1645			"msg.font": "Fuente principal",
1646			"msg.fontDisplay": "Fuente especial",
1647			"msg.rounded": "Redondeado",
1648			"msg.roundedBtns": "Botones redondeados",
1649			"msg.disableDefault" : "Deshabilitar plantilla por defecto",
1650			"msg.styles": "Estilos / css",
1651			"msg.cssImports": "CSS imports",
1652			"msg.css.settings": "Ajustes",
1653			"msg.css.variables": "Estilos de página y contenido",
1654			"msg.css.imports": "Importar fuentes o estilos",
1655			"msg.css.sections": "Estilos de sección",
1656			"msg.css.editor": "Editor CSS",
1657			"msg.variables": "Variables",
1658			"msg.title.plan": "Título planificar",
1659			"msg.title.result": "Título resultado",
1660			"msg.extension": "Extensión",
1661			"msg.trash": "Papelera",
1662			"msg.level": "Nivel",
1663			"msg.sys_changes": "Histórico de cambios",
1664			"msg.change.path": "Elemento modificado",
1665			"msg.change.old": "Antes",
1666			"msg.change.new": "Después",
1667			"msg.show.in.map": "Mostrar en el mapa",
1668			"msg.sync": "Sincronizar",
1669			"msg.public": "Público",
1670			"msg.uploadImage": "Subir imagen",
1671			"msg.uploadAudio": "Subir audio",
1672			"msg.uploadVideo": "Subir video",
1673			"msg.uploadFile": "Subir archivo",
1674			"msg.notify": "Notificar",
1675			"msg.set_value": "Asignar valor",
1676			"msg.copy_to_column": "Copiar a columna",
1677			"msg.multiActions": "Múltiples acciones",
1678			"msg.saveBeforeExecute": "Guardar antes de ejecutar",
1679			"msg.stock": "Stock",
1680			"msg.stock.exceeded": "Agotado",
1681			"msg.stock.show": "Mostrar stock",
1682			"msg.multiple": "Múltiple",
1683			"msg.youtube.url": "YouTube URL",
1684			"msg.playlist": "Lista de reproducción",
1685			"msg.property": "Propiedad",
1686			"msg.col": "Columna",
1687			"msg.row": "Fila",
1688			"msg.colNum": "Número de columna",
1689			"msg.rowNum": "Número de fila",
1690			"msg.customFilters": "Filtros personalizados",
1691			"msg.spreadsheet.query": "Query que se aplicará siempre",
1692			"msg.spreadsheet.query.sample": "ejemplo: name:like=john&age:$gt=18",
1693
1694			"msg.reload": "Recargar",
1695			"msg.reservation": "Compras",
1696			"msg.places": "Lugares",
1697			"msg.purchases": "Compras",
1698			"msg.reset": "Reiniciar",
1699			"msg.free": "Gratis",
1700			"msg.units": "Unidades",
1701			"msg.currency": "Moneda",
1702			"msg.price": "Precio",
1703			"msg.discount": "Descuento",
1704			"msg.quantity": "Cantidad",
1705			"msg.checkout": "Pagar",
1706			"msg.orders": "Mis pedidos",
1707			"msg.order": "Pedido",
1708			"msg.order.code": "Número de pedido",
1709			"msg.order.total": "Resumen del pedido",
1710			"msg.order.data": "Datos del pedido",
1711			"msg.order.items": "Artículos del pedido",
1712			"msg.order.new": "Nuevo pedido",
1713			"msg.order.list": "Lista de pedidos",
1714			"msg.order.base": "Subtotal",
1715			"msg.order.vat": "IVA",
1716			"msg.order.add.discount": "Añadir descuento",
1717			"msg.clients": "Clientes",
1718			"msg.client": "Cliente",
1719			"msg.operator": "Operador",
1720			"msg.note": "Nota",
1721			"msg.tips": "Propina",
1722			"msg.tips.allowed": "Propinas permitidas",
1723			"msg.tips.add": "Añadir propina:",
1724			"msg.anonymous.allowed": "Permitir comprar sin login",
1725			"msg.cart.userRequired": "Usuario requerido para añadir al carrito",
1726			"msg.customEntityOrder": "Entidad de pedidos propia",
1727			"msg.customEntityBooking": "Entidad de reservas propia",
1728			"msg.message" : "Mensaje",
1729		
1730			"msg.print": "Imprimir",
1731			"msg.print.next" : "Imprimir siguiente",
1732			"msg.pay": "Pagar",
1733			"msg.pay.mode": "Forma de pago",
1734			"msg.goToPay": "Iniciar proceso de pago",
1735			"msg.paid": "Pagado",
1736			"msg.paid.web": "Pagado en el web",
1737			"msg.delivered": "Entregado",
1738			"msg.to_pay": "Por pagar",
1739			"msg.to_deliver": "Por entregar",
1740			"msg.to_deliver_web": "Por entregar web",
1741			"msg.paymentMethods": "Métodos de pago",
1742			"msg.paymentMethods.choose": "Elija el método de pago:",
1743			"msg.paymentMethods.noAvailable": "No hay métodos de pago disponibles",
1744			"msg.payment.card.number": "Número de la tarjeta",
1745			"msg.payment.card.expiration": "Fecha de caducidad (MM/AA)",
1746			"msg.payment.card.control": "CVV/CVC",
1747			"msg.payment.declined": "Pago rechazado",
1748			"msg.payment.failed": "Pago no completado. El proceso de confirmación no se ha realizado correctamente, por lo que tu pago no ha sido efectivo. Por favor, vuelve a intentarlo para finalizar tu compra. Si el problema persiste, contacta con nuestro equipo de soporte.",
1749			"msg.booking": "Reserva",
1750			"msg.bookings": "Reservas",
1751			"msg.booking.title": "Haz una reserva",
1752			"msg.book": "Reservar",
1753			"msg.book.on": "Reservar en",
1754			"msg.capacity": "Aforo",
1755			"msg.max_bookings": "Max Reservas",
1756			"msg.capacity_per_booking": "Aforo por reserva",
1757			"msg.numPlaces": "Número de plazas",
1758			"msg.booking.day": "Día de la reserva",
1759			"msg.booking.time": "Hora de la reserva",
1760			"msg.booking.quantity": "Cantidad de la reserva",
1761			"msg.booking.success": "Su reserva ha sido procesado correctamente.",
1762			"msg.booking.date.not.enough.places": "No hay suficientes plazas libres. Elija otra fecha/hora.",
1763			"msg.booking.status.confirmed": "Confirmada",
1764			"msg.booking.status.pending_payment": "Pago pendiente",
1765			"msg.booking.status.canceled": "Cancelada",
1766			"msg.booking.cancel": "Cancelar reserva",
1767			"msg.booking.cancel.confirm.title": "Confirmar cancelación de la reserva",
1768			"msg.booking.cancel.confirm.text": "¿Estás seguro de que deseas cancelar esta reserva? No se puede deshacer.",
1769			"msg.goto.user.bookings": "Ver mis reservas",
1770		
1771			"msg.deliveryMethod": "Método de entrega",
1772			"msg.deliveryMethods": "Métodos de entrega",
1773			"msg.deliveryMethods.choose": "Elija el método de entrega:",
1774			"msg.delivery": "Entrega",
1775			"msg.deliveryMethod.entity.tip": "Seleccionar una entidad tipo Forma para pedir más datos de entrega o una entidad tipo Reservas para vincular la compra con una reserva",
1776			"msg.shippingCosts": "Gastos de envío",
1777		
1778			"msg.item.entity.tip": "Seleccionar una entidad tipo Forma para pedir más datos del producto en el proceso de compra",
1779		
1780			"msg.saved": "Guardado",
1781			"msg.created": "Creado",
1782			"msg.updated": "Actualizado",
1783			"msg.sys.updated": "Actualizado",
1784			"msg.removed": "Eliminado",
1785			"msg.restored": "Restaurado",
1786			"msg.moved": "Movido",
1787			"msg.copied": "Copiado",
1788			"msg.done": "Hecho",
1789			"msg.added": "Añadido",
1790			"msg.verified": "Verificado",
1791		
1792			"msg.yes": "Sí",
1793			"msg.no": "No",
1794			"msg.exists": "Existe",
1795			"msg.exit": "Salir",
1796			"msg.back": "Volver",
1797			"msg.next": "Siguiente",
1798			"msg.start": "Inicio",
1799			"msg.end": "Fin",
1800			"msg.finish": "Terminar",
1801			"msg.close": "Cerrar",
1802			"msg.accept": "Aceptar",
1803			"msg.accept.all": "Aceptar todo",
1804			"msg.reject": "Rechazar",
1805			"msg.cancel": "Cancelar",
1806			"msg.update": "Modificar",
1807			"msg.send": "Enviar",
1808			"msg.save": "Guardar",
1809			"msg.saveAndBack": "Guardar y Volver",
1810			"msg.saveAll": "Guardar todos",
1811			"msg.showAll": "Mostrar lista",
1812			"msg.remove": "Borrar",
1813			"msg.removeAll": "Borrar resultados",
1814			"msg.delete": "Eliminar",
1815			"msg.copy": "Copiar",
1816			"msg.copy.from": "Copiar de",
1817			"msg.copy.to": "Copiar a",
1818			"msg.copy.content": "Duplicar contenido",
1819			"msg.copy.system": "Duplicar entidades de sistema",
1820			"msg.edit": "Editar",
1821			"msg.add": "Añadir",
1822			"msg.add.to": "Añadir a",
1823			"msg.add.multiple": "Añadir multiples",
1824			"msg.add.multiple.auto": "Añadir auto",
1825			"msg.see": "Ver",
1826			"msg.change": "Cambiar",
1827			"msg.search": "Buscar",
1828			"msg.search.reset" : "Eliminar filtros",
1829			"msg.search.advanced": "Búsqueda avanzada",
1830			"msg.search.simple": "Búsqueda sencilla",
1831			"msg.search.placeholder": "Búsqueda",
1832			"msg.search.location": "Nombre, dirección o lat,long",
1833			"msg.create": "Crear",
1834			"msg.create_simple": "Crear simple",
1835			"msg.modeler": "Modelador",
1836			"msg.finalize": "Finalizar",
1837			"msg.upload": "Cargar",
1838			"msg.upload.preview": "Vista previa de la primera fila",
1839			"msg.upload.execute": "Esta bien! Guardar todas las filas",
1840			"msg.download": "Descargar",
1841			"msg.download.stats": "Descargar Stats",
1842			"msg.report": "Reporte",
1843			"msg.reports": "Reportes",
1844			"msg.move": "Mover",
1845			"msg.clone": "Clonar",
1846			"msg.merge": "Unir",
1847			"msg.new": "Nuevo",
1848			"msg.document": "Documento",
1849			"msg.documents": "Documentos",
1850			"msg.notUsed": "No utilizado",
1851			"msg.forbidden": "Prohibido",
1852			"msg.go": "Ir",
1853			"msg.clear": "Vaciar",
1854			"msg.logo": "Logo cabecera",
1855			"msg.logo.footer": "Logo pie de página",
1856			"msg.mode.test": "Test",
1857			"msg.attach_documents": "Adjuntar documentos",
1858			"msg.display_table": "Mostrar lista como tabla",
1859			"msg.display_table_web": "Mostrar lista como tabla (web)",
1860			"msg.normal": "Normal",
1861			"msg.read": "Leer",
1862			"msg.no_instance_required": "No se requiere instancia",
1863			"msg.no_user_required": "No se requiere usuario",
1864			"msg.myaccount": "Mostrar en mi cuenta",
1865			"msg.mycart": "Mostrar en mi carrito",
1866			"msg.home_super": "Mostrar en inicio de superusuario",
1867			"msg.home_instance": "Mostrar en inicio de todas las instancias",
1868			"msg.show_properties": "Mostrar propiedades",
1869			"msg.security" : "Seguridad",
1870			"msg.options" : "Opciones",
1871			"msg.tabs.hidden" : "Ocultar pestañas",
1872			"msg.change_status": "Cambio de estado",
1873			"msg.workflow": "Workflow",
1874			"msg.trigger": "Trigger",
1875			"msg.columnTimestamp": "Columna de fecha",
1876			"msg.columnStatus": "Columna de estado",
1877			"msg.resultType": "Tipo de resultado",
1878		
1879			"msg.section": "Sección",
1880			"msg.header": "Cabecera",
1881			"msg.footer": "Pie de página",
1882			"msg.footer.text": "Texto pie de página",
1883			"msg.background": "Fondo",
1884			"msg.secondary": "Secundario",
1885			"msg.card": "Tarjeta",
1886			"msg.template": "Plantilla",
1887			"msg.button": "Botón",
1888			"msg.config": "Config",
1889			"msg.pages": "Páginas",
1890			"msg.page": "Página",
1891			"msg.add.page": "Nueva página",
1892			"msg.add.item": "Nuevo elemento",
1893			"msg.action": "Acción",
1894			"msg.none": "Ninguna",
1895			"msg.instance.entity": "Entidad/Instancia",
1896			"msg.default": "Por defecto",
1897			"msg.template1": "Plantilla1",
1898			"msg.fullImage": "Imagen entera",
1899			"msg.imageTexts": "Imagen y textos",
1900			"msg.carousel": "Carrusel",
1901			"msg.gallery": "Galería",
1902			"msg.preview": "Vista previa",
1903			"msg.classes": "Clases",
1904			"msg.column": "Columna",
1905			"msg.left": "Izquierda",
1906			"msg.right": "Derecha",
1907			"msg.imageText": "Texto en imagen",
1908			"msg.horizontal": "Horizontal",
1909			"msg.enum.option": "Opción",
1910			"msg.restricted": "Restringido",
1911		
1912			"msg.select": "Seleccionar",
1913			"msg.select.all": "Seleccionar todos",
1914			"msg.select.search": "Seleccionar o buscar",
1915			"msg.select.media": "Seleccionar media",
1916			"msg.select.target": "Seleccionar objetivo",
1917			"msg.select.entity": "Seleccionar entidad",
1918			"msg.select.instance": "Selecciona una instancia",
1919			"msg.select.instance.last": "Últimas seleccionadas",
1920			"msg.select.from": "Seleccionar desde",
1921			"msg.select.addItem": "Añadir nuevo elemento",
1922		
1923			"msg.new.copy": "Copiar",
1924			"msg.new.search": "Buscar",
1925			"msg.new.multiple": "Multiples",
1926			"msg.new.showCreated": "Mostrar creados",
1927			"msg.new.hideCreated": "Ocultar creados",
1928			"msg.new.empty.extended": "Vacío extendido",
1929			"msg.new.locations": "Buscar ubicaciones",
1930			"msg.new.locations.auto": "Buscar ubicaciones auto",
1931			"msg.new.add": "Añadir existentes",
1932			
1933			"msg.new.version": "Hay una nueva actualización disponible",
1934			"msg.new.version.desc": "¿Desea actualizar ahora?",
1935		
1936			"msg.photos.add": "Añadir fotos",
1937			"msg.photos.google": "Fotos de Google",
1938			"msg.photos.wikipedia": "Fotos de Wikipedia",
1939		
1940			"msg.folder": "Sección",
1941			"msg.my_smart": "Agenda",
1942			"msg.new_smart": "Planificar",
1943		
1944			"msg.property.type.category": "Sección",
1945			"msg.property.type.text": "Texto",
1946			"msg.property.type.link": "Vínculo",
1947			"msg.property.type.boolean": "Booleano",
1948			"msg.property.type.number": "Número",
1949			"msg.property.type.date": "Fecha",
1950			"msg.property.type.color": "Color",
1951			"msg.property.type.tags": "Tags",
1952			"msg.property.type.list": "Lista",
1953			"msg.property.type.asin": "ASIN",
1954			"msg.property.type.hidden": "Oculto",
1955		
1956			"msg.column.default": "Por defecto",
1957			"msg.column.hidden": "Oculto",
1958			"msg.column.unexposed": "No expuesto",
1959			"msg.column.long": "Largo",
1960			"msg.column.html": "Html",
1961			"msg.column.tab": "Nueva pestaña",
1962			"msg.column.calendar": "Calendario",
1963			"msg.column.sort": "Lista ordenable",
1964			"msg.column.updatable": "Actualizable",
1965			"msg.column.display": "Mostrar",
1966			"msg.column.nolabel": "Sin título",
1967			"msg.column.required": "Requerido",
1968			"msg.column.readonly": "Solo lectura",
1969			"msg.column.search": "Buscar",
1970			"msg.column.user": "Usuario",
1971			"msg.column.uppercase": "Mayúsculas",
1972			"msg.column.autosave": "Autoguardado",
1973			"msg.column.unique": "Única",
1974		
1975			"msg.column.classes" : "Classes",
1976			"msg.column.size": "Tamaño",
1977			"msg.column.default_value": "Default value",
1978			"msg.column.number": "Number",
1979			"msg.column.min": "Min",
1980			"msg.column.max": "Max",
1981			"msg.column.maxlength": "Max length",
1982			"msg.column.minlength": "Min length",
1983			"msg.column.pattern": "Pattern",
1984			"msg.column.condition": "Condición (javascript)",
1985			"msg.column.delimiter": "Delimitador (o row-break)",
1986			"msg.column.buttonStyle": "Estilo del botón",
1987		
1988			"msg.group": "Grupo",
1989			"msg.system": "Sistema",
1990			"msg.configuration": "Configuración",
1991			"msg.myEntities": "Mis entidades",
1992			"msg.lists": "Listados",
1993		
1994			"msg.preferences": "Info",
1995			"msg.preferences.powered": "Powered by GEO®",
1996
1997			"msg.number.decimal.separator": "punto",
1998			"msg.currency.decimal.separator": "con",
1999			"msg.currency.USD.decimals": "centavos",
2000			"msg.currency.EUR.decimals": "céntimos",
2001			"msg.currency.MXN.decimals": "centavos",
2002
2003			// LOGIN
2004			"msg.login": "Iniciar sesión",
2005			"msg.login.google": "Iniciar sesión con Google",
2006			"msg.login.register": "¿Aún no tiene cuenta?",
2007			"msg.login.resetPwd": "Olvidé mi contraseña",
2008			"msg.login.error": "Datos incorrectos",
2009			"msg.register": "Crear cuenta",
2010			"msg.register.update": "Actualización de cuenta",
2011			"msg.register.update.desc": "Por favor, indique su email y revise las Condiciones de Servicio y Licencias",
2012			"msg.register.exists": "La cuenta ya existe. Por favor, intente restaurar su contraseña.",
2013			"msg.register.exists.validate": "La cuenta ya existe.",
2014			"msg.restore": "Restaurar",
2015			"msg.restore.title": "Crear nueva contraseña",
2016			"msg.restore.body" : "Hola,<br><br>Haz clic en este enlace para crear la contraseña de tu cuenta.<br><br><a href='{0}'>Crear contraseña</a><br><br>Si no has solicitado este cambio, ignora este correo electrónico.<br><br>Gracias",
2017			"msg.restore.sent1": "Por favor, revise su correo.",
2018			"msg.restore.sent2": "Habrá recibido un email con un link donde podrá indicar su contraseña.",
2019			"msg.restore.sent3": "Si no recibe el correo revise que el email sea correcto o espere 5 minutos por favor.",
2020			"msg.verify.title": "Cuenta no verificada",
2021			"msg.verify.desc": "Revise su correo para terminar la verificación de su cuenta por favor",
2022			"msg.verify.error": "La cuenta aun no está verificada",
2023			"msg.verify.btn": "Hecho",
2024			"msg.verify": "Verificar cuenta",
2025			"msg.verify.body" : "Hola,<br><br>Haga clic en este enlace para verificar su cuenta.<br><br><a href='{0}'>Verificar cuenta</a><br><br>Si no solicitó este cambio, ignore este correo electrónico.<br><br>Gracias.",
2026			"msg.disabled.register": "Nuevo usuario no permitido",
2027			"msg.password.new.invalid": "La nueva contraseña es inválida",
2028			"msg.licenses.accept": "He leido y acepto las Condiciones de Servicio y Licencias",
2029			"msg.logout": "Cerrar sessión",
2030		
2031			// CONFIRM
2032			"msg.confirm": "¿Está seguro que desea realizar esta operación?",
2033			"msg.confirm.removeInstance": "<span style='color:red'>Alerta!</span> <b>Esta operación va ha eliminar todos los datos relacionados.</b>",
2034			"msg.confirm.removeAll": "<span style='color:red'>Alerta!</span> <b>Esta operación va ha eliminar todos los resultados.</b>",
2035			"msg.confirm.leave": "¿Estás seguro de que quieres salir de esta página?",
2036		
2037			"msg.remove.warns.title": "Alerta!",
2038			"msg.remove.warns.to_remove": "Esta operación eliminará:",
2039			"msg.remove.warns.in_use": "Este objeto aún tiene relaciones con:",
2040		
2041			// COOKIES
2042			"msg.cookies.title": "Política de cookies",
2043			"msg.cookies.description": "<p>Usamos cookies para ofrecerle un uso correcto del sitio y de nuestros servicios.</p><p>Puede obtener más información en nuestra <a onclick='$angular.openLicense(\"cookies\", false)'>Política de cookies</a>.</p><p>Presione el botón ACEPTAR para confirmar que ha leído y aceptado la información.</p>",
2044		
2045			// TERMS
2046			"msg.terms.title": "Revisa nuestras Condiciones de Servicio y Licencias",
2047			"msg.terms.description": "Al continuar utilizando este servicio usted está de acuerdo con estos términos.",
2048			"msg.terms.cookies": "Política de cookies",
2049			"msg.terms.eula": "EULA",
2050			"msg.terms.privacy": "Política de privacidad",
2051			"msg.terms.terms": "Condiciones de Servicio",
2052			"msg.terms.platform": "Términos y Condiciones de Uso de la Plataforma Fidesur",
2053		
2054			// FORM VALIDATIONS
2055			"required": "Este campo es requerido.",
2056			"fields.required": "Campos requeridos.",
2057			"email.required": "Email requerido",
2058		
2059			// SERVER MESSAGES
2060			"server.alreadyExists": "ya existe",
2061			"server.entityExists": "El id '{0}' ya existe",
2062			"server.entityNotExists": "El id '{1}' no existe en '{0}'",
2063			"server.entity.allColumnsRequired": "Todas la columnas son requeridas",
2064			"server.entity.incorrectColumn": "Columna incorrecta: {0}",
2065			"server.entity.repeatedColumn": "Columna repetida: {0}",
2066			"server.geoFence.invalid": "Área inválido",
2067			"server.geo.id.required": "Id requerido",
2068			"server.geo.customId.invalid": "Id inválido '{0}'",
2069			"server.geo.hasData": "La entidad tiene datos en '{0}'",
2070			"server.geo.used": "Está siendo utilizado en {0} {1}",
2071			"server.geo.noInstance": "Instancia no seleccionada",
2072			"server.geo.name.required": "El nombre es requerido",
2073			"server.geo.instanceBelongItself": "La instancia no puede pertenecer a si mismo",
2074			"server.geo.instanceContainsItself": "La instancia no puede contenerse a si mismo",
2075			"server.media.typeRequired": "Tipo requerido",
2076			"server.media.noSourceLink": "Link de origen no encontrado",
2077			"server.media.sourceLink.incorrect": "Link de origen incorrecto",
2078			"server.media.sourceLink.noData": "Link de origen no tiene datos",
2079			"server.schedule.timeFrom.invalid": "La fecha inicial es inválida. Formato HH:mm",
2080			"server.schedule.timeTo.invalid": "La fecha final es inválida. Formato HH:mm",
2081			"server.schedule.date.required": "La fecha no puede estar vacía",
2082			"server.schedule.cron.invalid": "La expresión cron es inválida: {0}",
2083			"server.noEntities": "Entidades no definidas",
2084			"server.noEntities.found": "Entidades no encontradas",
2085			"server.words.required": "No datos que buscar",
2086			"server.area.required": "El area es requerida",
2087			"server.noPlacesFound": "Lugares no encontrados",
2088			"server.wikiPage.required": "La página de Wikipedia es requerida",
2089			"server.smarts.disabled": "El motor está deshabilitado",
2090			"server.spreadsheet.empty": "La hoja de cálculo no tiene filas",
2091
2092			// APP MESSAGES
2093			"msg.app.title": "GEO",
2094			"msg.app.description": "GEO",
2095		
2096			"msg.home": "Inicio",
2097			"msg.settings": "Configuración",
2098		
2099			"msg.instances": "Instancias",
2100			"msg.instance": "Instancia",
2101			"msg.subInstance": "SubInstancia",
2102			"msg.belongsTo": "Pertence a",
2103			"msg.contains": "Contiene",
2104			"msg.instance.home": "Inicio",
2105			"msg.instance.showCurrent": "Mostrar instancia actual",
2106			"msg.type.add": "Añadir tipo",
2107			"msg.entities": "Entidades",
2108			"msg.entity": "Entidad",
2109			"msg.entity.name": "Nombre de la entidad",
2110			"msg.entity.type": "Tipo de entidad",
2111			"msg.entity.value": "Entidad",
2112			"msg.users": "Usuarios",
2113			"msg.user": "Usuario",
2114			"msg.user.account": "Mi cuenta",
2115			"msg.user.qr": "Mi código QR",
2116			"msg.user.name": "Nombre del usuario",
2117			"msg.menu": "Menu",
2118			"msg.target": "Acción",
2119			"msg.recents": "Recientes",
2120			"msg.generateQR": "Ver código QR",
2121		
2122			"msg.translations": "Traducciones",
2123			"msg.translations.allowedVariables": "Este texto permite variables",
2124			"msg.translations.variables": "variables",
2125			"msg.translations.variables.info.text": "En este texto se permite añadir variables que serán resueltas con el valor que tengan en el momento de mostrar el texto.",
2126			"msg.translations.variables.info.list": "Variables habilitadas:",
2127			"msg.translations.variables.info.sample.title": "Uso de variables",
2128			"msg.translations.variables.info.sample.text": "Cada variable tiene asignado un número. En la parte del texto que queramos que se escriba la variable ponemos el número entre llaves. Ejemplo:",
2129			"msg.translations.variables.info.sample.original.label": "Guardamos el texto:",
2130			"msg.translations.variables.info.sample.original.value": "Hola {0}! Usted ha visitado {1} países!",
2131			"msg.translations.variables.info.sample.result.label": "Se resuelve cómo:",
2132			"msg.translations.variables.info.sample.result.value": "Hola Antonio! Usted ha visitado 15 países!",
2133			"msg.translations.media.description": "Permite sobreescribir el fichero multimedia por otro fichero para un idioma específico.",
2134		
2135			"msg.general": "General",
2136			"msg.sourceEntity": "Entidad fuente",
2137			"msg.resultEntities": "Entidades",
2138			"msg.use": "Usar",
2139			"msg.useTypes": "Usar tipos",
2140			"msg.usePriority": "Usar prioridad",
2141			"msg.useProximity": "Usar proximidad",
2142			"msg.usePeople": "Usar gente",
2143			"msg.useDates": "Usar fechas",
2144			"msg.useLocation": "Usar ubicación",
2145			"msg.useLocationInitial": "Usar ubicación inicial",
2146			"msg.useCategories": "Usar categorias",
2147			"msg.useTranslations": "Usar traducciones",
2148			"msg.dateFrom": "Fecha desde",
2149			"msg.dateTo": "Fecha hasta",
2150			"msg.until": "Hasta",
2151			"msg.timeFrom": "Hora desde",
2152			"msg.timeTo": "Hora hasta",
2153			"msg.people": "Gente",
2154			"msg.senior": "Mayores",
2155			"msg.adult": "Adultos",
2156			"msg.teen": "Jóvenes",
2157			"msg.kid": "Niños",
2158			"msg.dates": "Fechas",
2159			"msg.types": "Tipos",
2160			"msg.first_time": "Primera vez",
2161			"msg.been_here": "He estado aquí",
2162			"msg.live_here": "Vivo aquí",
2163			"msg.numResults": "Número de resultados",
2164			"msg.maxResults": "Max resultados",
2165			"msg.defaultTimeBetween": "Tiempo entre elementos",
2166			"msg.useAverageTime": "Usar tiempo promedio",
2167			"msg.defaultAverageTime": "Duración por defecto",
2168			"msg.includeRestaurants": "Incluir restaurantes",
2169			"msg.useInitialPosition": "Utilizar position inicial",
2170			"msg.maxDays": "Max días",
2171			"msg.defaultMaxDistance": "Max distancia por defecto",
2172			"msg.maxDistance": "Max distancia",
2173			"msg.walking": "Andando",
2174			"msg.driving": "Conduciendo",
2175			"msg.zoom": "Zoom",
2176			"msg.countryCode": "Código país",
2177			"msg.timezone": "Zona horaria",
2178			"msg.doNotPlan": "No planear",
2179		
2180			"msg.averageTime": "Tiempo promedio",
2181			"msg.outdoor": "Aire libre",
2182		
2183			"msg.info": "Info",
2184			"msg.location": "Ubicación",
2185			"msg.locationList": "Ubicaciones",
2186			"msg.list": "Lista",
2187			"msg.map": "Mapa",
2188			"msg.calendar": "Calendario",
2189			"msg.trivia": "Trivia",
2190			"msg.prize": "Premio",
2191			"msg.form": "Forma",
2192			"msg.buy": "Compra",
2193			"msg.buy.verb": "Comprar",
2194			"msg.buyer": "Comprador",
2195			"msg.identity": "Identidad",
2196			"msg.schedule": "Programar",
2197			"msg.schedules": "Horarios",
2198			"msg.properties": "Propiedades",
2199			"msg.media": "Media",
2200			"msg.media.add": "Añadir media",
2201			"msg.relations": "Relaciones",
2202			"msg.smarter": "Motor inteligente",
2203			"msg.smartRate": "Valores",
2204		
2205			"msg.latitude": "Latitud",
2206			"msg.longitude": "Longitud",
2207			"msg.mobile": "Móvil",
2208			"msg.indoorLevel": "Nivel interior",
2209			"msg.extraInfo": "Info extra",
2210			"msg.googlePlaceId": "ID de Google",
2211			"msg.timetables": "Horarios",
2212			"msg.timetable.invalidHours": "Horario inválido",
2213			"msg.website": "Sitio web",
2214		
2215			"msg.image": "Imagen",
2216			"msg.audio": "Audio",
2217			"msg.video": "Video",
2218			"msg.others": "Otros",
2219			"msg.images": "Imágenes",
2220			"msg.image.byUrl": "Desde url",
2221			"msg.licenses": "Licencias",
2222			"msg.license": "Licencia",
2223			"msg.author": "Autor",
2224			"msg.sourceLink": "Enlance de origen",
2225			"msg.sourceMediaUrl": "Url de origen",
2226			"msg.dropzone.drop": "Arrastre ficheros aquí o <label class='btn-link'>clic para seleccionar</label>",
2227			"msg.tags.placeholder": "Añadir tags",
2228			"msg.wikipedia": "Wikipedia",
2229			"msg.wikipedia.url": "Url de wikipedia",
2230			"msg.wikipedia.search": "Buscar en Wikipedia",
2231			"msg.wikiPage": "Página Wikipedia",
2232			"msg.google": "Google",
2233			"msg.flickr": "Flickr",
2234			"msg.seeOriginal": "Ver original",
2235			"msg.seeBig": "Ver en grande",
2236			"msg.seeResized": "Ver redimensionado",
2237			"msg.seeThumb": "Ver en pequeño",
2238			"msg.seeOnMap": "Ver en mapa",
2239		
2240			"msg.schedule.allDay": "Todo el día",
2241		
2242			"msg.searchLocation": "Buscar ubicación",
2243			"msg.searchLocations": "Buscar multiples ubicaciones",
2244			"msg.geofence": "Área",
2245			"msg.geofence.select": "Seleccione área",
2246			"msg.geofence.search": "Busca o dibuja área",
2247			"msg.geofence.radius": "Área por radio",
2248			"msg.geofence.draw": "Dibujar",
2249			"msg.geofence.drawMinimum": "3 puntos mínimo",
2250		
2251			"msg.locationList.dragToSort": "Arrastre para ordenar",
2252			"msg.locationList.routeUseFirstLocations": "La ruta solo utiliza las 10 primeras ubicaciones",
2253			"msg.locationList.calculateRoute": "Calcular ruta",
2254			"msg.locationList.calculateRouteSort": "Calcular ruta y ordenar",
2255			"msg.locationList.calculateRoute.walking": "Andando",
2256			"msg.locationList.calculateRoute.driving": "Conduciendo",
2257		
2258			"msg.columns": "Columnas",
2259			"msg.columns.add": "Añadir columna",
2260			"msg.columns.addEntity": "Añadir entitad",
2261			"msg.columns.default": "Columnas por defecto",
2262			"msg.entity.inner": "Entidades internas",
2263			"msg.instance.add": "Añadir nueva instancia",
2264		
2265			"msg.question": "Pregunta",
2266			"msg.answers": "Respuestas",
2267			"msg.answer.correct": "Correcto",
2268			"msg.answerExplanation": "Explicación",
2269			"msg.answers.is_survey": "Es encuesta",
2270		
2271			"msg.prize.image_inactive": "Imagen inactiva",
2272			"msg.prize.image_active": "Imagen activa",
2273		
2274			"msg.data": "Datos",
2275			"msg.google.data": "Datos de Google",
2276			"msg.wiki.data": "Datos de Wikipedia",
2277			"msg.place.exists": "Este lugar ya existe",
2278		
2279			"status.pending": "Por revisar",
2280			"status.working": "Trabajando",
2281			"status.resolved": "Resuelto",
2282			"status.accepted": "Aceptado",
2283			"status.rejected": "Rechazado",
2284		
2285			"day.sunday": "Domingo",
2286			"day.monday": "Lunes",
2287			"day.tuesday": "Martes",
2288			"day.wednesday": "Miércoles",
2289			"day.thursday": "Jueves",
2290			"day.friday": "Viernes",
2291			"day.saturday": "Sábado",
2292		
2293			"msg.time.format": "Tiempo HH:mm",
2294			"msg.distance.kms": "Distancia Kms",
2295		
2296			"msg.move.desc": "Esta operación mueve todos los registros asignados a la actual instancia a la entidad especificada.",
2297			"msg.similars.found": "Encontrados parecidos!",
2298			"msg.similars.found2": "Entidades parecidas encontradas",
2299			"msg.similars.found3": "¿Desea crearlo igualmente?",
2300		
2301			"msg.nearbyUsers.title": "Proximidad con otros usuarios",
2302			"msg.nearbyUsers.distance": "Distancia máxima (en metros)",
2303			"msg.nearbyUsers.time": "Diferencia de tiempo máxima (en minutos)",
2304		
2305			"msg.tax.includedInPrice": "Precios con impuestos incluidos",
2306			"msg.tax.vat": "Impuesto",
2307			"msg.tax.vat.percent": "Impuesto (%)",
2308		
2309			"msg.email.header": "Por favor, NO RESPONDER A ESTE CORREO.\nEste mensaje ha sido generado automáticamente.",
2310			"msg.email.footer": "Este mensaje está dirigido exclusivamente a su destinatario y puede contener información privilegiada o confidencial. Si no es usted el destinatario indicado, queda notificado de que la utilización, divulgación y/o copia sin autorización está prohibida en virtud de la legislación vigente. Si ha recibido este mensaje por error, le rogamos que nos lo comunique inmediatamente por esta misma vía y proceda a su destrucción.",
2311		
2312			"msg.scheduler.title": "Programar este evento",
2313			"msg.scheduler": "Planificador",
2314			"msg.recurrent": "Recurrente",
2315			"msg.allDay": "Todo el día",
2316			"msg.useDuration" : "Usar duración",
2317			"msg.day.from": "Fecha inicial",
2318			"msg.day.to": "Fecha final",
2319			"msg.starting": "Inicial",
2320			"msg.ending": "Final",
2321			"msg.duration": "Duración",
2322			"msg.blockDates": "Bloquear fechas",
2323			"msg.blockDates.description": "No podrá programar eventos en fechas bloqueadas",
2324			"msg.blockDays": "Bloquear días",
2325			"msg.blockHours": "Bloquear horas",
2326			"msg.generatedDates": "Fechas generadas",
2327			"msg.endDay": "Día final",
2328			"msg.endTime": "Hora de finalización",
2329			"msg.repeat": "Repetir",
2330			"msg.minutes": "Minutos",
2331			"msg.newDate": "Nueva fecha",
2332		
2333			"msg.timeAndAttendance": "Tiempo y asistencia",
2334			"msg.worked": "Trabajado",
2335			"msg.workedHours": "Horas trabajadas",
2336			"msg.extraHours": "Horas extra",
2337			"msg.attendantsEntity": "Entidad de asistentes",
2338			"msg.workdaysEntity": "Entidad de jornadas",
2339			"msg.accessRecordsEntities": "Entidades de registros de acceso",
2340			"msg.attendantColumn": "Columna asistente",
2341			"msg.dateColumn": "Column fecha",
2342			"msg.numFutureWorkdays": "Núm. jornadas a futuro",
2343			"msg.access.type": "Tipo de acceso",
2344			"msg.access.planned": "Hora planificada",
2345			"msg.access.record": "Hora de registro",
2346			"msg.difference": "Diferencia",
2347			"msg.access.type.in": "Entrada",
2348			"msg.access.type.out": "Salida",
2349			"msg.access.status.registered": "Registrado",
2350			"msg.access.status.unregistered": "No registrado",
2351			"msg.access.status.unplanned": "No planeado",
2352		
2353			"msg.action.login": "Iniciar sesión",
2354			"msg.action.logout": "Cerrar sesión",
2355			"msg.action.licenses": "Aceptar licencias",
2356			"msg.action.download": "Descargar",
2357			"msg.action.location": "Localizar",
2358			"msg.action.prize": "Ganar premio",
2359			"msg.action.qr": "Leer QR",
2360			"msg.action.details": "Ver detalles",
2361			"msg.action.favorite": "Marcar favorito",
2362			"msg.action.checkin": "Registrarse",
2363			"msg.action.rate": "Valorar",
2364			"msg.action.comment": "Comentar",
2365			"msg.action.play_audio": "Reproducir audio",
2366			"msg.action.play_video": "Reproducir video",
2367			"msg.action.share_media": "Compartir medios",
2368			"msg.action.smart": "Sistema experto",
2369			"msg.action.smart_create": "Sistema experto: Crear",
2370			"msg.action.smart_add": "Sistema experto: Añadir",
2371			"msg.action.smart_remove": "Sistema experto: Eliminar",
2372			"msg.action.smart_move": "Sistema experto: Mover",
2373			"msg.action.smart_move_to_day": "Sistema experto: Mover de día",
2374			"msg.action.trivia_answer": "Trivia: Responder pregunta",
2375			"msg.action.trivia_results": "Trivia: Ver resultados",
2376			"msg.action.survey": "Rellenar encuesta",
2377			"msg.action.book": "Reservar",
2378			"msg.action.buy": "Comprar orden",
2379		
2380			"msg.safeemployee": "Safe Employee",
2381		
2382			"msg.hide_media": "Ocultar media",
2383			"msg.calendar_date": "Fecha para el calendario",
2384		
2385			"msg.scheduler.minutely": "Por minutos",
2386			"msg.scheduler.hourly": "Por horas",
2387			"msg.scheduler.daily": "Diario",
2388			"msg.scheduler.weekly": "Semanal",
2389			"msg.scheduler.monthly": "Mensual",
2390			"msg.scheduler.yearly": "Anual",
2391			"msg.scheduler.advanced": "Avanzado",
2392			"msg.scheduler.every": "Cada",
2393			"msg.scheduler.on": "en el",
2394			"msg.scheduler.onThe": "En el",
2395			"msg.scheduler.at": "a las",
2396			"msg.scheduler.of": "de",
2397			"msg.scheduler.and": "y",
2398			"msg.scheduler.second": "segundo",
2399			"msg.scheduler.seconds": "segundo(s)",
2400			"msg.scheduler.minute": "minuto",
2401			"msg.scheduler.minutes": "minuto(s)",
2402			"msg.scheduler.hour": "hora",
2403			"msg.scheduler.hours": "hora(s)",
2404			"msg.scheduler.day": "día",
2405			"msg.scheduler.days": "día(s)",
2406			"msg.scheduler.month": "mes",
2407			"msg.scheduler.months": "mes(es)",
2408			"msg.scheduler.weekDay": "día de la semana (Lunes a Viernes)",
2409			"msg.scheduler.dayOfWeek.MON": "Lunes",
2410			"msg.scheduler.dayOfWeek.TUE": "Martes",
2411			"msg.scheduler.dayOfWeek.WED": "Miércoles",
2412			"msg.scheduler.dayOfWeek.THU": "Jueves",
2413			"msg.scheduler.dayOfWeek.FRI": "Viernes",
2414			"msg.scheduler.dayOfWeek.SAT": "Sábado",
2415			"msg.scheduler.dayOfWeek.SUN": "Domingo",
2416			"msg.scheduler.cronExpression": "Expresion cron",
2417			"msg.scheduler.cronExpressionDetails": "Puede encontrar más detalles sobre como crear estas expresiones",
2418			"msg.scheduler.here": "aquí",
2419			"msg.scheduler.monthDay.1W": "Primer día de la semana",
2420			"msg.scheduler.monthDay.LW": "Último día de la semana",
2421			"msg.scheduler.monthDay.L": "Último día",
2422			"msg.scheduler.monthWeek.#1": "Primer",
2423			"msg.scheduler.monthWeek.#2": "Segundo",
2424			"msg.scheduler.monthWeek.#3": "Tercero",
2425			"msg.scheduler.monthWeek.#4": "Cuarto",
2426			"msg.scheduler.monthWeek.#5": "Quinto",
2427			"msg.scheduler.monthWeek.L": "Último",
2428			"msg.scheduler.month.1": "Enero",
2429			"msg.scheduler.month.2": "Febrero",
2430			"msg.scheduler.month.3": "Marzo",
2431			"msg.scheduler.month.4": "Abril",
2432			"msg.scheduler.month.5": "Mayo",
2433			"msg.scheduler.month.6": "Junio",
2434			"msg.scheduler.month.7": "Julio",
2435			"msg.scheduler.month.8": "Agosto",
2436			"msg.scheduler.month.9": "Septiembre",
2437			"msg.scheduler.month.10": "Octubre",
2438			"msg.scheduler.month.11": "Noviembre",
2439			"msg.scheduler.month.12": "Diciembre",
2440		
2441			// FRONT END MESSAGES
2442			"msg.web.whatDo": "¿Qué hacer?",
2443			"msg.web.know": "Conoce",
2444			"msg.web.knowMore": "Conoce más",
2445			"msg.web.destinations": "Destinos",
2446			"msg.web.seeAll": "Ver todos",
2447			"msg.web.seeMore": "Ver más",
2448			"msg.web.select": "¿Dónde quiere ir?",
2449			"msg.web.recommendations": "¿Quieres recomendaciones personalizadas?",
2450			"msg.web.discover": "Descubre nuestros destinos",
2451			"msg.web.filters": "Filtros",
2452		
2453			"msg.web.cart": "Carrito de compra",
2454			"msg.web.cart.empty": "Su carrito está vacío",
2455			"msg.web.cart.checkout": "Pagar",
2456			"msg.web.cart.validateCard": "Validar tarjeta",
2457			"msg.web.cart.finish": "Finalizar pago",
2458			"msg.web.cart.continue": "Continuar comprando",
2459			"msg.web.cart.done.desc": "Su pedido ha sido procesado correctamente.",
2460			"msg.web.cart.contact": "Contacto",
2461			"msg.web.cart.identity.guest": "Tramitar compra como invitado",
2462			"msg.web.cart.identity.user": "Tramitar compra con su cuenta",
2463
2464			"msg.web.plan": "Crear agenda",
2465			"msg.web.plan.new": "Nuevo",
2466			"msg.web.plan.people": "¿Quién viaja?",
2467			"msg.web.plan.types": "¿Lo visitó anteriormente?",
2468			"msg.web.plan.location": "Ubicación",
2469			"msg.web.plan.categories": "Preferencias",
2470			"msg.web.plan.dates": "Fechas",
2471			"msg.web.plan.results": "Ver resultados",
2472		
2473			"msg.web.agenda": "Mi Agenda",
2474			"msg.web.agenda.open": "Abierto",
2475			"msg.web.agenda.duration": "Duración",
2476			"msg.web.agenda.noResults": "No se han encontrado resultados",
2477		
2478			"msg.web.footer": "Powered by GEO®"
2479		}
2480
2481	}
2482	
2483}
2484
2485//// File constants.js ////
2486var $constants = {};
2487
2488// TODO save data in database?
2489$constants.clean = function() {
2490	$constants.entityList = {};
2491	$constants.data = {}; // getData (min)
2492	$constants.dataRaw = {}; // getDataRaw (all)
2493	$constants.names = {};
2494	$constants.categoryList = {};
2495	$constants.entities = null; // getEntities	
2496};
2497$constants.clean(); // init
2498
2499$constants.removeFromCache = function(entity, entityId) {
2500	if (entity == 'entity') {
2501		delete $entity.metas[entityId];
2502	} else if (entity == 'category') {
2503		// TODO		
2504	}
2505	delete $constants.names[entityId];
2506	delete $constants.data[entityId];
2507	delete $constants.dataRaw[entityId];
2508};
2509
2510
2511// Constants
2512$constants.blank = "--blank--";
2513
2514$constants.formatDay = 'dd MMM yyyy';
2515$constants.formatDayTime = 'dd/MM/yyyy - HH:mm';
2516
2517$constants.noIds = ['new', 'multiple', 'multiple-locations', 'auto'];
2518
2519$constants.languages = ['en', 'es']; // , 'fr'
2520
2521$constants.productList = ["safeemployee", "time-and-attendance", "microsoft-accounts"];
2522
2523//$constants.templateList = {
2524//	'default': "msg.default"
2525//};
2526
2527$constants.daysList = {
2528	0: "day.sunday",
2529	1: "day.monday",
2530	2: "day.tuesday",
2531	3: "day.wednesday",
2532	4: "day.thursday",
2533	5: "day.friday",
2534	6: "day.saturday"
2535};
2536
2537$constants.enabledList = {
2538	'true': "msg.yes",
2539	'false': "msg.no"
2540};
2541
2542$constants.rolesList = {
2543	'super': "role.super",
2544	'admin': "role.admin",
2545	'coordinator': "role.coordinator",
2546	'basic': "role.basic"
2547};
2548
2549$constants.rolesList2 = {
2550	'super': "role.super",
2551	'admin': "role.admin",
2552	'coordinator': "role.coordinator"
2553};
2554
2555$constants.uses = {
2556	100: "100%",
2557	75: "75%",
2558	50: "50%",
2559	25: "25%",
2560	0: "msg.notUsed"
2561};
2562
2563$constants.ratings = {
2564	"100": "100%",
2565	"75": "75%",
2566	"50": "50%",
2567	"25": "25%",
2568	"0": "0%",
2569	"-": "msg.forbidden"
2570};
2571
2572$constants.menuTargets = {
2573	'folder': 'msg.section',
2574	'entity': 'msg.entity.type',
2575	'instance': 'msg.instance',
2576	'my_smart': 'msg.my_smart',
2577	'new_smart': 'msg.new_smart',
2578	'my_account': 'msg.user.account',
2579	'my_qr': 'msg.user.qr',
2580	'link': 'msg.link'
2581};
2582
2583// home item types
2584$constants.itemTypes = {
2585	'default': 'Full text',
2586	'full_image': 'Full image',
2587	'column': 'Vertical image/text',
2588	'preview': 'Preview',
2589	'carousel': 'Carousel',
2590	'preview-carousel': 'Preview carousel',
2591	'gallery': 'Gallery',
2592	'form': 'Form',
2593	'formview': 'Formview',
2594	'video': 'Video',
2595	'youtube': 'Youtube',
2596	'instagram': 'Instagram',
2597	'pure_html': 'Pure HTML',
2598	'dashboard': 'Dashboard',
2599	'empty': 'Empty'
2600};
2601
2602$constants.userActions = {
2603	'login': 'msg.action.login',
2604	'logout': 'msg.action.logout',
2605	'licenses': 'msg.action.licenses',
2606	'download': 'msg.action.download',
2607	'location': 'msg.action.location',
2608	'prize': 'msg.action.prize',
2609	'qr': 'msg.action.qr',
2610	'details': 'msg.action.details',
2611	'favorite': 'msg.action.favorite',
2612	'checkin': 'msg.action.checkin',
2613	'rate': 'msg.action.rate',
2614	'comment': 'msg.action.comment',
2615	'play_audio': 'msg.action.play_audio',
2616	'share_media': 'msg.action.share_media',
2617	'smart': 'msg.action.smart',
2618	'smart_create': 'msg.action.smart_create',
2619	'smart_add': 'msg.action.smart_add',
2620	'smart_remove': 'msg.action.smart_remove',
2621	'smart_move': 'msg.action.smart_move',
2622	'smart_move_to_day': 'msg.action.smart_move_to_day',
2623	'trivia_answer': 'msg.action.trivia_answer',
2624	'trivia_results': 'msg.action.trivia_results',
2625	'survey': 'msg.action.survey',
2626	'book': 'msg.action.book',
2627	'cart': 'msg.action.buy',
2628	'buy': 'msg.action.buy'
2629};
2630
2631// home item actions
2632$constants.itemActions = {
2633	'entity': 'msg.instance.entity',
2634	'link': 'msg.link',
2635	'my_smart': 'msg.my_smart',
2636	'new_smart': 'msg.new_smart'
2637};
2638$constants.itemAlignment = {
2639	'left': 'left',
2640	'center': 'center',
2641	'right': 'right'
2642};
2643
2644$constants.entityTypes = {
2645	"entity": "msg.app_object",
2646	"listing": "msg.listing",
2647	"system": "msg.system"
2648};
2649
2650// custom inner objects
2651$constants.bindingList = {
2652	"geo_user": "msg.user",
2653	"geo_location": "msg.location",
2654	"geo_fence": "msg.geofence",
2655	"geo_schedule": "msg.schedule",
2656	"geo_trivia": "msg.trivia",
2657	"geo_prize": "msg.prize",
2658	"geo_form": "msg.form",
2659	"geo_buy": "msg.buy",
2660	"geo_calendar": "msg.calendar"
2661};
2662
2663// entity checks
2664$constants.entityOptions = [
2665	'create_simple', 'attach_documents', 'show_properties', 'show_key_values', 'hide_media', 'display_table', 'display_table_web', 
2666	'home_super', 'home_instance', 'auto_location', 'duplicate_copy', 
2667	'one_item', 'readonly', 'modeler', 'lazyformulas' ];
2668
2669$constants.entitySecurity = [
2670	'read', 'create', 'update', 'delete', 'no_instance_required', 'no_user_required', 'myaccount', 'mycart' ];
2671
2672$constants.editTabs = [
2673	'geo-admin', 'geo-buy', 'geo-location', 'geo-schedule', 'geo-trivia', 'geo-prize', 'geo-categories', 
2674	'geo-smart-rate', 'geo-translations', 'geo-actions' ];
2675
2676$constants.inheritances = [ 'no_instance', 'current', 'children', 'parents', 'parents_children', 'all_related_instances' ];
2677
2678// column checks
2679$constants.columnChecks = [
2680	'default', 'tab', 'display', 'search', 
2681	'autosave', 'long', 'nolabel', 'unique', 
2682	'required', 'readonly', 'updatable', 'hidden', 'unexposed', 
2683	'html', 'uppercase', 'calendar', 'sort', 'lazyformula',
2684	'user', 'super', 'admin', 'admincreate' ];
2685
2686$constants.columnChecksValue = [
2687	'classes', 'size', 'default_value', 'minlength', 'maxlength', 'pattern', 'min', 'max', 'condition' ];
2688
2689$constants.columnTypeList1 = {
2690	"string":"format_bold", 
2691	"string_list":"format_size", 
2692	"text":"translate", 
2693	"number":"numbers", 
2694	"boolean":"check_box", 
2695	"boolean_int":"check_box", 
2696	"enum_list":"library_add_check",
2697	"enum":"radio_button_checked", 
2698	"select":"filter_list", 
2699	"date":"event", 
2700	"time":"access_time", 
2701	"datetime":"update", 
2702	"money":"attach_money",
2703	"percentage":"looks_5",
2704	"email":"email",
2705	"phone":"phone", 
2706	"password":"password", 
2707	"password_raw":"password",
2708	"color":"format_color_fill",
2709	"cron":"edit_calendar",
2710	"json":"code", 
2711	"css":"code", 
2712	"image":"image", 
2713	"video":"play_circle", 
2714	"audio":"audiotrack", 
2715	"document":"upload_file",
2716	"any":"help_center", 
2717};
2718$constants.columnTypeList2 = {
2719	"entity":"description", 
2720	"inner_entity":"description", 
2721	"relation_entity":"description", 
2722	"any_entity":"description",
2723	 
2724	"list":"library_books", 
2725	"inner_entity_list":"library_books", 
2726	"relation":"library_books", 
2727	"any_entity_list":"library_books",
2728	
2729	"formula":"calculate",
2730	"table":"grid_on", 
2731};
2732$constants.columnTypeList3 = {
2733	"group":"group_work", 
2734	"button":"smart_button", 
2735	"link":"link", 
2736	"map":"place", 
2737	"chart":"bar_chart",
2738	"categories":"list",  
2739	"geo_schedule":"date_range", 
2740	"geo_properties":"notes",
2741	"message":"title", 
2742	"h1":"title", 
2743	"h2":"title", 
2744	"h3":"title", 
2745	"h4":"title", 
2746	"h5":"title", 
2747	"blank":"check_box_outline_blank",
2748};
2749$constants.columnTypeListForm = { 
2750	"ui_image":"collections", 
2751	"ui_video":"video_library", 
2752	"ui_audio":"library_music", 
2753	"ui_document":"content_copy", 
2754	"ui_relation":"library_books", 
2755	"timetables":"date_range", 
2756//	"schedules":"date_range",
2757	"booking":"event", 
2758	"buy":"shopping_cart", 
2759};
2760
2761$constants.propertyTypes = {
2762	'section': 'msg.section',
2763	'string': 'msg.property.type.text',
2764	'link': 'msg.property.type.link',
2765	'boolean': 'msg.property.type.boolean',
2766	'number': 'msg.property.type.number',
2767	'color': 'msg.property.type.color',
2768	'asin': 'msg.property.type.asin'
2769};
2770
2771$constants.buttonActionList = {
2772	"save": "msg.save",
2773	"reset": "msg.reset",
2774	"back": "msg.back",
2775	"link": "msg.link",
2776	"set_value": "msg.set_value",
2777	"move": "msg.move",
2778	"copy": "msg.copy",
2779	"copy_to_column": "msg.copy_to_column",
2780	"download": "msg.download",
2781	"print": "msg.print",
2782	"datetime": "msg.datetime",
2783	"smarter": "msg.smarter",
2784	"notify": "msg.notify",
2785	"sync": "msg.sync",
2786	"change_status": "msg.change_status",
2787	"trigger": "msg.trigger",
2788	"actions": "msg.multiActions"
2789};
2790
2791$constants.getEntityList = function(type, search, add, omitInstances, limit, query, addEmpty) {
2792
2793	if ($utils.isBlank(type)) {
2794		return;
2795	}
2796	if (typeof type != 'string') {
2797		$angular.log("[getEntityList] " + type + " is not string")
2798		return;
2799	}
2800
2801	if (type === "entity" && $instance.current && $instance.current.entityList) {
2802		var isInstance = $angular.contentScope && $angular.contentScope.entity === 'instance';
2803		if (!isInstance || !$user.isSuper()) {
2804			var list = $instance.current.entityList;
2805			if (search) {
2806				list = list.filter(function(a) {
2807					var name = $entity.displayColumn(a);
2808					return name && name.toLowerCase().contains(search);
2809				});
2810			}
2811			list.sort(function(a, b) {
2812				var n1 = $entity.displayColumn(a);
2813				var n2 = $entity.displayColumn(b);
2814				if (n1 < n2) {
2815					return -1;
2816				}
2817				if (n1 > n2) {
2818					return 1;
2819				}
2820				return 0;
2821			});
2822			$constants.entityList[type] = list;
2823			return;
2824		}
2825	}
2826
2827	var index = 0;
2828	var loading = null;
2829	if (add && $constants.entityList[type]) {
2830		index = $constants.entityList[type].length - 1;
2831		loading = 'generic';
2832	}
2833	if (limit === undefined || limit === null) {
2834		limit = 250;
2835	}
2836	var paramSearch = "";
2837	if (!$utils.isBlank(search)) {
2838		paramSearch = "&_q=" + $utils.removeDiacritics(search.toLowerCase());
2839	}
2840	if (omitInstances) {
2841		paramSearch += "&_omit_instances=true";
2842	}
2843	if (query) {
2844		paramSearch += "&" + query;
2845	}
2846	var url;
2847	if (!$constants.isWeb) {
2848		url = contextPath + "/api/geo/" + type + "/a/light.json?_limit=" + limit + "&_index=" + index + paramSearch;
2849	} else {
2850		url = contextPath + "/www/geo/" + type + "/a/light.json?_limit=" + limit + "&_index=" + index + paramSearch;
2851	}
2852	return $rest.get({url: url, loading: loading}).then(function(list) {
2853		
2854		for (var k in list) {
2855			var item = list[k];
2856			$constants.names[item.id] = $constants.getEntityDesc(type, item);
2857		}
2858
2859		if (add && $constants.entityList[type]) {
2860			$constants.entityList[type].pop();
2861			$constants.entityList[type] = $constants.entityList[type].concat(list);
2862		} else {
2863			$constants.entityList[type] = list;
2864			if (addEmpty && $utils.isBlank(search)) {
2865				$constants.entityList[type].unshift({id: $constants.blank, name: $constants.blank});
2866			}
2867		}
2868		if (list.length === 50) {
2869			$constants.entityList[type].push({id: null, name: '...'});
2870		}
2871		return $constants.entityList[type];
2872
2873	}, function(o) {
2874		var status = 0;
2875		if (o && o.response && o.response.status) {
2876			status = o.response.status;
2877		}
2878		if (status != 401 && status != 403) {
2879			//$init.logError('Failed load entities ' + type + ' - status ' + status);
2880		}
2881
2882		if (status > 500) {
2883			// can be deploying -> retry
2884			$constants.getEntityList(type, search, add, omitInstances, limit, query, addEmpty);
2885		}
2886	});
2887};
2888
2889$constants.getDataListMin = function(entity, query, index, limit, omitInstances) {
2890	var paramSearch = "";
2891	if (query) {
2892		paramSearch += "&" + query;
2893	}
2894	if (omitInstances) {
2895		paramSearch += "&_omit_instances=true";
2896	}
2897	var url;
2898	if (!$constants.isWeb) {
2899		url = contextPath + "/api/geo/" + entity + "/a/light.json?_limit=" + limit + "&_index=" + index + paramSearch;
2900	} else {
2901		url = contextPath + "/www/geo/" + entity + "/a/light.json?_limit=" + limit + "&_index=" + index + paramSearch;
2902	}
2903	return $rest.get({
2904		url: url
2905	}).then(function(list) {
2906		list.forEach(function(o) {
2907			$constants.data[o.id] = o;
2908		});
2909		return list;
2910	});
2911};
2912
2913$constants.getEntityDesc = function(entity, item) {
2914	
2915	if ($utils.isBlank(item)) {
2916		return ""; // no warning
2917	}
2918	var $translate = $angular.get("$translate");
2919	if (item.name === "...") {
2920		return '<em>-- ' + $translate.instant("msg.showMoreOptions") + ' --</em>';
2921	}
2922	
2923	if (typeof item != 'string') {
2924		
2925		if ($utils.isArray(item)) {
2926			var result = [];
2927			for (var k in item) {
2928				var r = $entity.displayColumn(item[k]);
2929				if (!$utils.isBlank(r)) {
2930					result.push(r);
2931				}
2932			}
2933			return $utils.join(result, ", ");
2934		}
2935		
2936		if (item.id) {
2937			var val = $entity.displayColumn(item);
2938			if (!$utils.isBlank(val)) {
2939				return val;
2940			}
2941			item = item.id;
2942		
2943		} else if (item._id) {
2944			item = item._id;
2945		}
2946		if (typeof item == 'number') {
2947			item = "" + item;
2948		}
2949		if (typeof item != 'string') {
2950			console.log('[getEntityDesc] bad arguments: ' + entity, item);
2951			//$angular.log('[getEntityDesc] bad arguments: ' + entity + " - type:" + JSON.stringify(item), item);
2952			//return "bad_arguments";
2953			return "";
2954		}
2955	}
2956	if ($utils.isBlank(item)) {
2957		return "";
2958	}
2959	if ($utils.isBlank(entity)) {
2960		return item;
2961	}
2962	if (item.contains("/")) {
2963		return item + "*";
2964	}
2965
2966	var value = $constants.names[item];
2967	if (value) {
2968		//console.log(entity + ":" + item + " => " + value)
2969		return value;
2970	}
2971	
2972	var dat = $constants.data[item];
2973	if (dat) {
2974		value = $entity.displayColumn(dat);
2975		if (!$utils.isBlank(value)) {
2976			$constants.names[item] = value;
2977			return value;
2978		}
2979	}
2980
2981	if (entity === "entity") {
2982		if (item === "entity") {
2983			value = $translate.instant("msg.entity");
2984			$constants.names[item] = value;
2985			return $utils.parse(value);
2986		}
2987		if (item === "trash") {
2988			value = $translate.instant("msg.trash");
2989			$constants.names[item] = value;
2990			return value;
2991		}
2992		if (item === "ips") {
2993			value = $translate.instant("msg.ips");
2994			$constants.names[item] = value;
2995			return value;
2996		}
2997
2998		if (!$instance.hasToRefresh) {
2999			value = $instance.getEntityName(item);
3000		}
3001		if (!$utils.isBlank(value)) {
3002			$constants.names[item] = value;
3003			return $utils.parse(value);
3004		}
3005	}
3006	if (entity === "category") {
3007		value = $instance.getCategoryName(item);
3008		if (!$utils.isBlank(value)) {
3009			$constants.names[item] = value;
3010			return $utils.parse(value);
3011		}
3012	}
3013	if ($constants.names[item] != null) { // multiple requests and empty results
3014		return;
3015	}
3016
3017	// get data by item
3018	$constants.names[item] = "";
3019	//console.log('getting ' + entity + " - " + item)
3020	var url;
3021	if (!$constants.isWeb) {
3022		url = contextPath + "/api/geo/" + entity + "/" + item + "/name.json";
3023	} else {
3024		url = contextPath + "/www/geo/" + entity + "/" + item + "/name.json";
3025	}
3026	$rest.get({url: url, showError: false}).then(function(o) {
3027		//console.log('get ' + entity+"."+item, o)
3028		if ($utils.isBlank(o)) o = item; // "" TODO show id if empty for selects?
3029		$constants.names[item] = o;
3030
3031	}, function(o) {
3032
3033		var status = 0;
3034		if (o && o.response && o.response.status) {
3035			status = o.response.status;
3036		}
3037
3038		if (status > 500) {
3039			// can be deploying -> retry
3040			delete $constants.names[item];
3041			return undefined; // to avoid in deploys
3042		}
3043
3044		// unknown error -> end
3045		$constants.names[item] = item + " (" + status + ")";
3046	});
3047
3048	return undefined;
3049};
3050
3051$constants.isDisabled = function(entity, item) {
3052	var enabled = $constants.isEnabled(entity, item);
3053	if (enabled === undefined) {
3054		return undefined;
3055	}
3056	return !enabled;
3057};
3058
3059$constants.isEnabled = function(entity, item) {
3060	if (item === undefined) {
3061		return true;
3062	}
3063	if ($utils.isBlank(entity) || $utils.isBlank(item) || typeof item != 'string') {
3064		//$angular.log('[isEnabled] bad arguments: ' + entity + " - type:" + (typeof item), item);
3065		return false;
3066	}
3067	var dat = $constants.getDataMin(entity, item);
3068	if (dat) {
3069		return !!dat.enabled;
3070	}
3071	return undefined;
3072};
3073
3074$constants.getEntityListDesc = function(entity, list) {
3075	var cats = [];
3076	if (list) {
3077		for (var key in list) {
3078			var item = $constants.getEntityDesc(entity, list[key]);
3079			if (item == undefined) {
3080				return undefined;
3081			}
3082			if (item) {
3083				cats.push(item);
3084			}
3085		}
3086	}
3087	return $utils.join(cats, ", ");
3088};
3089
3090$constants.getEntityDescAsPromise = function(entity, item, counter) {
3091	var deferred = $angular.get('$q').defer();
3092	var entityDesc = $constants.getEntityDesc(entity, item);
3093	if (entityDesc === undefined) {
3094		if (counter === 10) {
3095			return deferred.reject();
3096		}
3097		var next = counter ? (counter + 1) : 1;
3098		setTimeout(function() {
3099			$constants.getEntityDescAsPromise(entity, item, next).then(function(result) {
3100				deferred.resolve(result);
3101			});
3102		}, 200);
3103	} else {
3104		deferred.resolve(entityDesc);
3105	}
3106	return deferred.promise;
3107};
3108
3109$constants.getDefaultColumnRaw = function(entity, id, callback) {
3110	var url = contextPath + "/api/geo/" + entity + "/" + id + "/nameRaw.json";
3111	$rest.get({url: url, showError: false}).then(function(o) {
3112		callback(o);
3113	});
3114};
3115
3116$constants.getCategoryList = function(type, search, add, all) {
3117	if (search == undefined) {
3118		search = '';
3119	}
3120	var index = 0;
3121	var loading = null;
3122	if (add && $constants.categoryList[type]) {
3123		index = $constants.categoryList[type].length - 1;
3124		loading = 'generic';
3125	}
3126	var limit = "50";
3127	if (all) {
3128		limit = "0";
3129	}
3130	var url;
3131	if (!$constants.isWeb) {
3132		url = contextPath + "/api/geo/category/a/light.json";
3133	} else {
3134		url = contextPath + "/www/geo/category/a/light.json";
3135	}
3136	url += "?_limit=" + limit + "&=50&_index=" + index + "&_order=name&_q="
3137		+ $utils.removeDiacritics(search.toLowerCase()) + "&enabled=true&entities=" + type;
3138	$rest.get({url: url, loading: loading}).then(function(list) {
3139		if (add && $constants.categoryList[type]) {
3140			$constants.categoryList[type].pop();
3141			$constants.categoryList[type] = $constants.categoryList[type].concat(list);
3142		} else {
3143			$constants.categoryList[type] = list;
3144		}
3145		if (list.length == 50) {
3146			$constants.categoryList[type].push({id: null, name: '...'});
3147		}
3148	}, function(o) {
3149		var status = 0;
3150		if (o && o.response && o.response.status) {
3151			status = o.response.status;
3152		}
3153		//$init.logError('Failed load categories ' + type + " - error " + status);
3154
3155		if (status > 500) {
3156			// can be deploying -> retry
3157			$constants.getCategoryList(type, search, add, all);
3158		}
3159	});
3160};
3161
3162$constants.getCategoryListDesc = function(list) {
3163	var cats = [];
3164	if (list) {
3165		if (typeof list == "string") {
3166			return $constants.getEntityDesc('category', list);
3167		}
3168		if (!$utils.isArray(list)) {
3169			return "";
3170		}
3171		for (var key in list) {
3172			var item = $constants.getEntityDesc('category', list[key]);
3173			if (item == undefined) {
3174				return undefined;
3175			}
3176			if (item && item != list[key]) {
3177				cats.push(item);
3178			}
3179		}
3180	}
3181	return $utils.join(cats, ", ");
3182};
3183
3184//$constants.getEntities = function() {
3185//	if ($constants.entities != null) {
3186//		return $constants.entities;
3187//	}
3188//	if ($instance.getId() != null) {
3189//		var list = $utils.clone($instance.getEntityList());
3190//		list.unshift({id: 'entity', name: $angular.get("$translate").instant("msg.entity")});
3191//		$constants.entities = list;
3192//		return $constants.entities;
3193//	}
3194//	$constants.entities = [];
3195//	var url = contextPath + "/api/geo/entity/a/light.json";
3196//	$rest.get({url: url}).then(function(list) {
3197//		var list = $utils.cloneList(list);
3198//		list.unshift({id: 'entity', name: $angular.get("$translate").instant("msg.entity")});
3199//		$constants.entities = list;
3200//	}, function(o) {
3201//
3202//		var status = 0;
3203//		if (o && o.response && o.response.status) {
3204//			status = o.response.status;
3205//		}
3206//		//$init.logError('Failed load entities for columns - error ' + status);
3207//
3208//		if (status > 500) {
3209//			// can be deploying -> retry
3210//			$constants.getEntities();
3211//		}
3212//	});
3213//	return $constants.entities;
3214//};
3215
3216$constants.getDataMin = function(entity, id, callback) {
3217	var o = $constants.data[id];
3218	if (o) {
3219		if (o.id == null) {
3220			return null;
3221		}
3222		if (callback) {
3223			callback(o);
3224		}
3225		return o;
3226	}
3227	
3228	$constants.data[id] = {}; // to avoid multiple requests
3229	
3230	//console.log('getting entity ' + entity + " " + id);
3231	var url;
3232	if (!$constants.isWeb) {
3233		url = contextPath + '/api/geo/'+entity+'/' + id + '/min.json';
3234	} else {
3235		url = contextPath + '/www/geo/'+entity+'/' + id + '/min.json';
3236	}
3237	if ($instance.current) {
3238		url += '?geo_instance='+$instance.getId();
3239	}
3240	$rest.get({
3241		url: url,
3242		showError: false
3243	}).then(function(o) {
3244		$constants.data[o.id] = o;
3245		if (callback) {
3246			callback(o);
3247		}
3248	}, function(o) {
3249		var status = 0;
3250		if (o && o.response && o.response.status) {
3251			status = o.response.status;
3252		}
3253		if ($constants.names[id] == null) {
3254			$constants.names[id] = id + " (" + status + ")";
3255		}
3256		if (callback) {
3257			callback(null);
3258		}
3259	});
3260	return null;
3261};
3262
3263$constants.getDataRaw = function(entity, id, callback) {
3264	var o = $constants.dataRaw[id];
3265	if (o) {
3266		if (o.id == null) {
3267			return null;
3268		}
3269		if (callback) {
3270			callback(o);
3271		}
3272		return o;
3273	}
3274	
3275	$constants.dataRaw[id] = {}; // to avoid multiple requests
3276	
3277	//console.log('getting entity ' + entity);
3278	var url;
3279	if (!$constants.isWeb) {
3280		url = contextPath + '/api/geo/'+entity+'/' + id + '.json';
3281	} else {
3282		url = contextPath + '/www/geo/'+entity+'/' + id + '.json';
3283	}
3284	if ($instance.current) {
3285		url += '?geo_instance='+$instance.getId();
3286	}
3287	$rest.get({
3288		url: url,
3289		showError: false
3290	}).then(function(o) {
3291		$constants.dataRaw[o.id] = o;
3292		if (callback) {
3293			callback(o);
3294		}
3295	});
3296	return null;
3297};
3298
3299//// File rest.js ////
3300var $rest = {};
3301
3302$rest.fetchAPI = false; // TODO bug safari with cookies
3303//console.log('fetchAPI: ' + $rest.fetchAPI)
3304
3305$rest.version = appVersion;
3306
3307$rest.get = function(params) {
3308	params = $.extend(params, { method:"GET" });
3309	return $rest.request(params);
3310};
3311
3312$rest.post = function(params) {
3313	params = $.extend(params, { method:"POST" });
3314	return $rest.request(params);
3315};
3316
3317$rest.put = function(params) {
3318	params = $.extend(params, { method:"PUT" });
3319	return $rest.request(params);
3320};
3321
3322$rest.remove = function(params) {
3323	params = $.extend(params, { method:"DELETE" });
3324	return $rest.request(params);
3325};
3326
3327$rest.options = function(params) {
3328	params = $.extend(params, { method:"OPTIONS" });
3329	return $rest.request(params);
3330};
3331
3332$rest.request = function(params) {
3333
3334	if (params == null) {
3335		params = {};
3336	}
3337	if (params.skipHideLoading) {
3338		$angular.rootScope.skipHideLoading = true;
3339	}
3340
3341	var defer = $angular.get("$q").defer();
3342	params.defer = defer;
3343
3344	if (params.confirmTitle || params.confirmText) {
3345
3346		// with confirm panel
3347		$rest.openConfirmPanel(params.confirmTitle, params.confirmText,
3348				function() { $rest.execute(params); }, params.cancelFunction);
3349
3350	} else {
3351		// without confirm panel
3352		$rest.execute(params);
3353	}
3354
3355	return defer.promise;
3356};
3357
3358$rest.requests = new Array();
3359
3360$rest.execute = function(params) {
3361
3362	if (params.method == undefined || params.method == "") {
3363		throw "The parameter 'method' is required.";
3364	}
3365	if (params.url == undefined || params.url == "") {
3366		throw "The parameter 'url' is required.";
3367	}
3368	if (params.showError == undefined) {
3369		params.showError = true;
3370	}
3371
3372	// control multiple same request TODO can generate no responses...
3373	if (!$utils.isTrue(params.force)) {
3374		if ($rest.requests.indexOf(params.url) != -1) {
3375			return;
3376		}
3377		$rest.requests.push(params.url);
3378	}
3379	
3380	var headers = null;
3381	var data = null;
3382	if (params.noContentType) {
3383		// nothing
3384		headers = params.headers;
3385
3386	} else if (params.contentType == "urlencoded") {
3387		// urlencoded
3388		headers = { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' };
3389		if (params.data) {
3390			data = $.param(params.data);
3391		} else if (params.form) {
3392			data = $(params.form).serialize();
3393		}
3394
3395	} else {
3396		// default json
3397		headers = { 'Content-Type': 'application/javascript' };
3398		if (params.data) {
3399			data = params.data;
3400		} else if (params.form) {
3401			data = $utils.serializeFormJSON($(params.form));
3402		}
3403
3404		if (params.method == "POST" || params.method == "PUT") {
3405			if ($angular.contentScope && $angular.contentScope.model) {
3406				var model = $angular.contentScope.model;
3407				if (model.entity && model.id && model.sys_updated) {
3408					if (data == null) {
3409						data = {};
3410					}
3411					data['_sys_updated'] = model.entity + "#" + model.id + "#" + model.sys_updated;
3412					if (data['sys_updated'] != null) {
3413						delete data['sys_updated'];
3414					}
3415				}
3416			}
3417		}
3418	}
3419	if (headers == null) {
3420		headers = {};
3421	}
3422	if (params.instance) {
3423		headers['From'] = params.instance;
3424	} else if ($instance.current != null) {
3425		headers['From'] = $instance.getId();
3426	}
3427	//headers['ALLOW-FROM'] = "www";
3428	headers['ALLOW-FROM'] = ""+window.location.href;
3429
3430	if (params.loading || params.skipHideLoading) {
3431		$utils.showLoading(params.loading);
3432	}
3433	
3434	if (!$rest.fetchAPI) {
3435		var configHttp = {
3436			method : params.method,
3437			url : params.url,
3438			data : data,
3439			headers : headers
3440		};
3441		if (params.responseType) {
3442			configHttp.responseType = params.responseType;
3443		}
3444		$angular.get("$http")(configHttp).then(function(response) {
3445	    	$rest.processResult(params, response);
3446	    }, function(response) {
3447	    	$rest.processResult(params, response);
3448	    });
3449		return;
3450	}
3451
3452	if (data && typeof data != 'string') {
3453		data = angular.toJson(data);
3454	}
3455
3456	//console.log(params.url, data)
3457	fetch(params.url, {
3458		method: params.method,
3459		headers: headers,
3460		body: data
3461	}).then(function(response) {
3462		$rest.processData(params, response);
3463	}).catch(function(e) {
3464		//console.log('Error get data ' + params.url, e);
3465		$rest.processResult(params, { status:0 });
3466	});
3467
3468};
3469
3470$rest.processData = function(params, response) {
3471	
3472	if (response.status > 0) {
3473		var contentType = $rest.getHeader(response, "content-type");
3474		if (contentType == 'application/json') {
3475			response.json().then(function(data) {
3476				response.data = data; // save in response
3477				$rest.processResult(params, response);
3478			}).catch(function() {
3479				$rest.processResult(params, response);
3480			});
3481		} else {
3482			response.text().then(function(data) {
3483				response.data = data; // save in response
3484				$rest.processResult(params, response);
3485			}).catch(function() {
3486				$rest.processResult(params, response);
3487			});
3488		}
3489		return;
3490	}
3491	
3492	$rest.processResult(params, response);
3493}
3494
3495$rest.processResult = function(params, response) {
3496	
3497	if (response == null || response.status == null) {
3498		response = { status:0 };
3499	}
3500	
3501	var data = response.data;
3502	//console.log('> ' + response.status + " " + params.url, data)
3503	
3504	if (data && data._logs) {
3505		//$angular.consoleScope.consoleList = ""; // clear logs
3506		$angular.consoleScope.addLog(data._logs);
3507		delete data._logs;
3508	}
3509	
3510	if (params.skipHideLoading) {
3511		$angular.rootScope.skipHideLoading = false; // to skip not found location pages after loaded
3512	}
3513	if (params.loading || params.skipHideLoading) {
3514		$utils.hideLoading(params.loading);
3515	}
3516
3517	var v1 = $rest.version;
3518	var v2 = $rest.getHeader(response, "version");
3519	if (!$utils.isBlank(v2) && v2 > v1) {
3520		console.log('new version: ' + v1 + " -> " + v2)
3521		$rest.version = v2;
3522
3523		$angular.get("$timeout")(function() {
3524			if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
3525				navigator.serviceWorker.controller.postMessage({key: 'new_version'})
3526			}
3527			console.log('show new version panel: ' + $rest.showNewVersion)
3528			if ($rest.showNewVersion) {
3529				setTimeout(function() {
3530					$rest.openConfirmPanel("msg.new.version", "msg.new.version.desc", function() {
3531						window.location.reload();
3532					});
3533				}, 1000); // TODO wait to precache
3534			}
3535		}, 1000);
3536	}
3537
3538	if (!$utils.isTrue(params.force)) {
3539		// remove the doble submit control
3540		$rest.requests.splice($rest.requests.indexOf(params.url), 1);
3541	}
3542	
3543	// switch http codes
3544	switch(response.status) {
3545
3546	// success
3547	case 100: case 101: case 102:
3548	case 200: case 201: case 202: case 203: case 204:
3549	case 205: case 206: case 207:
3550
3551		if (params.successMessage != null) {
3552			$utils.floatingMsg(params.successMessage);
3553		}
3554
3555		if (params.fullResponse) {
3556			params.defer.resolve(response);
3557		} else {
3558			var location = $rest.getHeader(response, "location");
3559			params.defer.resolve(data, location);
3560		}
3561		break;
3562
3563	case 401:
3564		var path = "" + $angular.get("$location").path(); // path is not string???
3565		var isLoginPage = path.contains('/login');
3566		if (!isLoginPage && params.showError) {
3567			$angular.tmpPath = path;
3568			$angular.goLogin();
3569			return;
3570		}
3571		params.defer.reject({msg:'Authentication required', response:response});
3572		break;
3573
3574	case 403:
3575		var msg = $rest.showError(params, data, $angular.get("$translate").instant("msg.accessDenied"));
3576		params.defer.reject({msg:msg, response:response});
3577		break;
3578
3579	case 404:
3580		var msg = $rest.showError(params, data, $angular.get("$translate").instant("msg.notFound"));
3581		params.defer.reject({msg:msg, response:response});
3582		break;
3583
3584	case 0:
3585	case -1:
3586	case null:
3587	case undefined:
3588		var msg = $rest.showError(params, data, $angular.get("$translate").instant("msg.error.noConnection"));
3589		params.defer.reject({msg:msg, response:response});
3590		break;
3591
3592	// error
3593	default:
3594		var msg = $rest.showError(params, data, null);
3595		params.defer.reject({msg:msg, response:response});
3596	}
3597
3598};
3599
3600$rest.getHeader = function(response, header) {
3601	if (response.headers) {
3602		if ($rest.fetchAPI) {
3603			// TODO for .. of error in old versions
3604			var it = response.headers.entries();
3605			var result = it.next();
3606			while (!result.done) {
3607				var pair = result.value;
3608				//console.log(pair[0] + " = " + pair[1])
3609				if (pair[0] === header) {
3610					return pair[1];
3611				}
3612 				result = it.next();
3613			}
3614		} else {
3615			return response.headers(header);	
3616		}
3617	}
3618	return null;
3619};
3620
3621$rest.showError = function(params, data, defMsg) {
3622	
3623	if (data && !$utils.isBlank(data.message)) {
3624		data = $angular.get("$translate").instant(data.message);
3625	}
3626	
3627	if (data) {
3628		if (typeof data == "string") {
3629			if (data.contains("502 Bad Gateway")) {
3630				data = $angular.get("$translate").instant("msg.error.502");
3631			
3632			} else if (data.indexOf('<html') != -1) {
3633				$angular.log("Request error: " + params.url + " : " + data);
3634				data = null;
3635			}
3636		} else {
3637			console.log(JSON.stringify(data));
3638			data = null;
3639		}
3640	}
3641	
3642	if (params.showError) {
3643		var msg = "";
3644		if (!$utils.isBlank(defMsg)) {
3645			msg = defMsg;
3646		}
3647		if (data != null) {
3648			if (msg != "") {
3649				msg += "<br><small>" + data + "</small>";
3650			} else {
3651				msg = data;
3652			}
3653		}
3654		if ($utils.isBlank(msg)) {
3655			msg = $angular.get("$translate").instant("msg.error");
3656		}
3657		if (params.skipHideLoading) {
3658			$utils.showAlert(msg);
3659		} else {
3660			$utils.floatingError(msg);
3661		}
3662	}
3663
3664	return data;
3665};
3666
3667$rest.openConfirmPanel = function(title, text, successFunction, cancelFunction) {
3668	var $translate = $angular.get("$translate");
3669	$angular.openModal("panel-confirm", {
3670		title: $translate.instant(title),
3671		text: $translate.instant(text),
3672		successFunction: successFunction,
3673		cancelFunction: cancelFunction
3674	});
3675};
3676
3677$rest.getFileName = function(response) {
3678	var disposition = $rest.getHeader(response, 'Content-Disposition');
3679	if (disposition && disposition.indexOf('attachment') !== -1) {
3680		var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
3681		var matches = filenameRegex.exec(disposition);
3682		if (matches != null && matches[1]) {
3683			return matches[1].replace(/['"]/g, '');
3684		}
3685	}
3686	return null;
3687};
3688
3689//// File events.js ////
3690
3691
3692var $events = {};
3693
3694//$events.lastScrolls = [];
3695
3696$events.refresh = function(resetBacks) {
3697	
3698	$angular.get("$timeout")(function() {
3699		
3700		// left menu
3701		if (!$constants.isWeb) {
3702			$('#menuToggle').unbind('click').on('click', function() {
3703				$events.toggleMenu();
3704			});
3705		} else {
3706			$('#menuToggle, #menu .menu-close, #menu .menu-background, #menu a').unbind('click').on('click', function(){
3707				$events.toggleMenu();
3708			});
3709		}
3710
3711		// esc close panels
3712		$events.manageEsc();
3713		
3714		// reset scroll
3715		window.scrollTo(0, 0);
3716		setTimeout(function() {
3717			window.scrollTo(0, 0);
3718			
3719			if ($angular.isBackNavigation) {
3720				$angular.isBackNavigation = false;
3721				var name = window.location.href;
3722				var pos = $angular.positions[name];
3723				if (pos && pos > 0) {
3724					$events.scrollIfReady(pos, 0);
3725				}
3726			}
3727		}, 100);
3728		
3729	});
3730
3731};
3732$events.scrollIfReady = function(pos, num) {
3733	if ($angular.ready) {
3734		window.scrollTo(0, pos);
3735		
3736		var $window = $angular.get("$window");
3737		var diff = Math.abs($window.pageYOffset - pos);
3738		//console.log("scrollTo: " + pos + " diff:"+diff);
3739		if (diff < 20) {
3740			return;
3741		}
3742	}
3743	if (num == 20) {
3744		return;
3745	}
3746	setTimeout(function() {
3747		$events.scrollIfReady(pos, num+1);
3748	}, 50);
3749};
3750
3751//$events.removeLastScroll = function() {
3752//	var page = $angular.get('$location').path();
3753//	if ($utils.isBlank(page)) {
3754//		return;
3755//	}
3756//	//console.log('remove scroll ' + page)
3757//	$events.lastScrolls[page] = 0;
3758//};
3759//
3760//$events.manageLastScroll = function() {
3761//	
3762//	var page = $angular.get('$location').path();
3763//	if ($utils.isBlank(page)) {
3764//		return;
3765//	}
3766// 	
3767//	var ls = $events.lastScrolls[page];
3768//	if (ls == null) {
3769//		ls = 0;
3770//	}
3771//	//console.log('setting: ' + page + ": " + ls)
3772////	window.scrollTo(0, ls);
3773////	setTimeout(function() {
3774////		window.scrollTo(0, ls);
3775////	}, 100);
3776////	setTimeout(function() {
3777////		window.scrollTo(0, ls);
3778////	}, 150); // masonry has timeout 120
3779////	setTimeout(function() {
3780////		window.scrollTo(0, ls);
3781////	}, 300); // sometimes is taking more time to load
3782////	if (!$constants.isWeb) {
3783////		setTimeout(function() {
3784////			window.scrollTo(0, ls);
3785////		}, 500); // sometimes is taking more time to load
3786////	}
3787//	
3788//	$events.updateLastScroll();
3789//};
3790//
3791//$events.updateLastScroll = function() {
3792//	
3793//	var page = $angular.get('$location').path();
3794//	if ($utils.isBlank(page)) {
3795//		return;
3796//	}
3797//	
3798//	var ww = $angular.get('$window');
3799//	angular.element(window).on('scroll', function(event){
3800//		$events.lastScrolls[page] = ww.pageYOffset;
3801//		//console.log(page + ": " + ww.pageYOffset)
3802//	});
3803//	
3804//	// reset scrolls
3805//	if ($angular.currentRoute == "web-home") {
3806//		$events.lastScrolls = []; 
3807//	}
3808//
3809//};
3810
3811$events.back = function() {
3812	// TODO control no exit
3813	if (window.history.length > 0) {
3814		window.history.back();
3815		return;
3816	}
3817	$angular.goHome();
3818};
3819
3820$events.toggleMenu = function() {
3821	$('#menuToggle').toggleClass('active');
3822	$('body').toggleClass('body-push-toleft');
3823	$('#menu').toggleClass('menu-open');
3824	
3825	if (!$constants.isWeb) {
3826		$storageService.put("_menu_open", $('#menu').hasClass("menu-open"));
3827	}
3828};
3829
3830$events.toggleCartAsMenu = function() {
3831	$('#cartAsMenuToggle').toggleClass('active');
3832	$('body').toggleClass('body-push-toright');
3833	$('#cartAsMenu').toggleClass('menu-open');
3834};
3835
3836$events.manageEsc = function() {
3837	$(document).unbind('keyup').keyup(function(e) {
3838		if (e.keyCode == 27) {
3839			$events.closeAll(true);
3840		}
3841	});
3842};
3843
3844$events.closeAll = function(isUser) {
3845	var scopes = [$angular.contentScope, $angular.rootScope];
3846	for (var k in scopes) {
3847		var scope = scopes[k];
3848		if (scope && scope.$$phase) { // controlling digest in progress
3849			setTimeout(function() {
3850				$events.closeAll(isUser);
3851			}, 250);
3852			return;
3853		}
3854	}
3855	if ($(".modal").length > 0) {
3856		$(".modal .close").click();
3857	}
3858	if ($(".dropdown.open").length > 0) {
3859		$(".dropdown.open > .dropdown-toggle").click();
3860	}
3861	if ($(".dropdown.show").length > 0) {
3862		$(".dropdown.show > .dropdown-toggle").click();
3863	}
3864	if ($constants.isWeb) {
3865		if ($("#menu").hasClass("menu-open")) {
3866			$('#menuToggle').click();
3867		}
3868	}
3869	if ($("#cartAsMenu").hasClass("menu-open")) {
3870		$('#cartAsMenuToggle').click();
3871	}
3872	if ($("#searchForm button.closeSearch").is(":visible")) {
3873		$("#searchForm button.closeSearch").click();
3874	}
3875	if ($("#images-fullscreen").is(":visible")) {
3876		$("#images-fullscreen .close").click();
3877	}
3878	if (isUser) {
3879		if ($(".alert.floating-msg").length > 0) {
3880			$(".alert.floating-msg .close").click();
3881		}
3882		if ($("#search button.closeSearch").is(":visible")) {
3883			$("#search button.closeSearch").click();
3884		}
3885		if ($("#console").length > 0) {
3886			$("#console").click();
3887		}
3888	}
3889};
3890
3891//// File cart.js ////
3892var $cart = {};
3893var cartInitialized = false;
3894var cartInitializedCallbacks = [];
3895
3896$cart.initCart = function() {
3897	cartInitialized = false;
3898	$angular.rootScope.cart = null;
3899	if ($constants.isWeb && $instance.hasCart()) {
3900		var anonymousCartId = $cart.getAnonymousCartId();
3901		var url = contextPath + '/www/cart.json';
3902		if (anonymousCartId) {
3903			url = contextPath + '/www/cart/' + anonymousCartId + '.json';
3904		}
3905		var initializedCart = function(o) {
3906			$angular.rootScope.cart = o ? o : {};
3907			cartInitialized = true;
3908			cartInitializedCallbacks.forEach(function(callback) {
3909				callback();
3910			});
3911			cartInitializedCallbacks = [];
3912			$angular.apply($angular.rootScope);
3913		};
3914		if ($user.isLogged() || $cart.isAllowedAnonymous() || !$cart.isUserRequiredToAddCart()) {
3915			$rest.get({
3916				url: url,
3917				showError: false
3918			}).then(function(o) {
3919				if (!o || $user.isLogged()) {
3920					$cart.setAnonymousCartId(null);
3921				}
3922				initializedCart(o);
3923			}).catch(function() {
3924				$cart.setAnonymousCartId(null);
3925				initializedCart(null);
3926			});
3927		} else {
3928			initializedCart(null);
3929		}
3930	} else {
3931		//$angular.log('No instance cart enabled');
3932	}
3933};
3934
3935$cart.doWhenCartWasInitialized = function(callback) {
3936	if (cartInitialized) {
3937		callback();
3938	} else {
3939		cartInitializedCallbacks.push(callback);
3940	}
3941};
3942
3943$cart.hasItems = function() {
3944	return $cart.getItemsQuantity() > 0;
3945}
3946
3947$cart.getItemsQuantity = function() {
3948	var cart = $cart.get();
3949	var num = 0;
3950	if (cart) {
3951		for (var i in cart.items) {
3952			var item = cart.items[i];
3953			num += item.quantity;
3954		}
3955	}
3956	return num;
3957};
3958
3959$cart.getTotalAmount = function() {
3960	var cart = $cart.get();
3961	return cart ? cart.amount : null;
3962};
3963
3964$cart.getItems = function() {
3965	var cart = $cart.get();
3966	return cart ? cart.items : null;
3967};
3968
3969$cart.buy = function(e, item) {
3970	$utils.preventDefault(e);
3971	if ($user.isLogged() || !$cart.isUserRequiredToAddCart()) {
3972		
3973		var formEntity = item.geo_buy ? item.geo_buy.form_entity : null;
3974		if (formEntity && $entity.isGeoForm(formEntity) && $entity.isPermitted(formEntity, 'create')) {
3975			// open popup to show extra data
3976			$angular.openModal('panel-buy', {
3977				item: item,
3978				cartItem: $cart.getCartItem(item.entity, item.id),
3979				size: item.geo_buy.form_entity ? 'xl' : null
3980			});
3981		} else {
3982			// add directly to cart
3983			var cartItem = $cart.getCartItem(item.entity, item.id);
3984			if (cartItem == null) {
3985				cartItem = {quantity: 0, extraData: {}};
3986			}
3987			var hasStock = $cart.hasStock(item, cartItem.quantity);
3988			if (hasStock) {
3989				cartItem.quantity++;
3990				$cart.addItem(item.entity, item.id, cartItem);
3991			}
3992			if (!$angular.element("#cartAsMenu").hasClass("menu-open")) {
3993				$events.toggleCartAsMenu();
3994			}
3995		}
3996		
3997	} else {
3998		$user.openLoginAsPanel(function() {
3999			$cart.buy(e, item);
4000		});
4001	}
4002};
4003
4004$cart.getProductPrice = function(item) {
4005
4006	var o = item;
4007	if (item && item.id && item.entity) {
4008		item = item.geo_buy;
4009	}
4010	if (item == null) {
4011		return "";
4012	}
4013	if ($utils.isTrue(item.free)) {
4014		return '<b>' + $utils.parseGeoTranslation('free', $instance.current, 'msg.free', 'geo_buy') + '</b>';
4015	}
4016
4017	if (!$cart.hasStock(o)) {
4018		return '<b>' + $utils.parseGeoTranslation('stock_exceeded', $instance.current, 'msg.stock.exceeded', 'geo_buy') + '</b>';
4019	}
4020
4021	var priceToPrint = $utils.printCurrency($utils.addTaxes(item.price, item.tax));
4022	if (!item.discount || isNaN(item.discount) || Number(item.discount) === 0) {
4023		return '<b>' + priceToPrint + '</b>';
4024	}
4025	var newPriceToPrint = $utils.printCurrency($utils.addTaxes(item.price - item.discount, item.tax));
4026	return '<b class="small tach">' + priceToPrint + '</b>' + ' <b>' + newPriceToPrint + '</b>';
4027};
4028
4029$cart.getItemAmount = function(item, currency) {
4030	if (item == null) {
4031		return "";
4032	}
4033	if ($utils.isTrue(item.free)) {
4034		return "<b>" + $utils.parseGeoTranslation('free', $instance.current, 'msg.free', 'geo_buy') + "</b>";
4035	}
4036	return $utils.printCurrency(item.amount, currency);
4037};
4038
4039$cart.externalPaymentScripts = {};
4040
4041$cart.get = function() {
4042	return $angular.rootScope.cart;
4043};
4044
4045$cart.reset = function() {
4046	$angular.rootScope.cart = {};
4047	$cart.setAnonymousCartId(null);
4048	$cart.save();
4049};
4050$cart.resetRaw = function() {
4051	$angular.rootScope.cart = {};
4052	$cart.setAnonymousCartId(null);
4053};
4054
4055$cart.save = function() {
4056	var url = contextPath + '/www/cart.json';
4057	if (!$user.isLogged()) {
4058		var anonymousCartId = $cart.getAnonymousCartId();
4059		if (anonymousCartId) {
4060			url = contextPath + '/www/cart/' + anonymousCartId + '.json';
4061		}
4062	}
4063	$rest.put({
4064		url: url,
4065		data: $angular.rootScope.cart,
4066		force: true
4067	}).then(function(o) {
4068		$angular.rootScope.cart = o ? o : {};
4069		var id = o == null || $user.isLogged() ? null : o.id;
4070		$cart.setAnonymousCartId(id);
4071		$angular.apply($angular.rootScope);
4072	});
4073};
4074
4075$cart.pay = function(data) {
4076	var cart = $cart.get();
4077	return $rest.post({
4078		url: contextPath + '/www/cart/' + cart.id + '/pay.json',
4079		data: data,
4080		loading: 'generic'
4081	});
4082};
4083
4084$cart.complete = function(data) {
4085	var cart = $cart.get();
4086	return $rest.post({
4087		url: contextPath + '/www/cart/' + cart.id + '/complete.json',
4088		data: data,
4089		loading: 'generic',
4090		showError: false
4091	}).then(function(order) {
4092		$cart.reset();
4093		return order;
4094	}).catch(function() {
4095		return null;
4096	});
4097};
4098
4099$cart.getCartItem = function(productEntity, productId) {
4100	var cart = $cart.get();
4101	if (cart && cart.items) {
4102		for (var i in cart.items) {
4103			var cartItem = cart.items[i];
4104			if (cartItem.product.entity === productEntity && cartItem.product.entity_id === productId) {
4105				return cartItem;
4106			}
4107		}
4108	}
4109	return null;
4110};
4111
4112$cart.addItem = function(productEntity, productId, data) {
4113	var cartItem = $cart.getCartItem(productEntity, productId);
4114	if (cartItem) {
4115		cartItem.quantity = data.quantity;
4116		cartItem.extra_data = data.extraData;
4117	} else {
4118		var cart = $cart.get();
4119		if (!cart) {
4120			cart = {};
4121			$angular.rootScope.cart = cart;
4122		}
4123		if (!cart.items) {
4124			cart.items = [];
4125		}
4126		cart.items.push({
4127			product: {
4128				entity: productEntity,
4129				entity_id: productId
4130			},
4131			quantity: data.quantity,
4132			extra_data: data.extraData
4133		});
4134	}
4135	$cart.save();
4136};
4137
4138$cart.removeItem = function(cartItem) {
4139	var cart = $cart.get();
4140	if (cart && cart.items) {
4141		cart.items = cart.items.filter(function(item) {
4142			return item.product.entity !== cartItem.product.entity ||
4143				item.product.entity_id !== cartItem.product.entity_id;
4144		});
4145		$cart.save();
4146	}
4147};
4148
4149$cart.setDeliveryMethod = function(deliveryMethod) {
4150	var cart = $cart.get();
4151	if (cart) {
4152		cart.delivery_method = deliveryMethod;
4153		$cart.save();
4154	}
4155};
4156
4157$cart.isDeliveryMethodType = function(type) {
4158	var cart = $cart.get();
4159	if (cart && cart.delivery_method) {
4160		return cart.delivery_method.type === type;
4161	}
4162	return !type;
4163};
4164
4165$cart.getDeliveryMethodBooking = function() {
4166	var cart = $cart.get();
4167	return cart && cart.delivery_method ? cart.delivery_method.booking : null;
4168};
4169
4170$cart.setDeliveryMethodBooking = function(booking) {
4171	var cart = $cart.get();
4172	if (cart && cart.delivery_method) {
4173		cart.delivery_method.booking = booking;
4174		$cart.save();
4175	}
4176};
4177
4178$cart.getDeliveryMethodExtraData = function() {
4179	var cart = $cart.get();
4180	return cart && cart.delivery_method ? cart.delivery_method.extra_data : null;
4181};
4182
4183$cart.setDeliveryMethodExtraData = function(entity, extraData) {
4184	var cart = $cart.get();
4185	if (cart && cart.delivery_method) {
4186		cart.delivery_method.extra_data_entity = entity;
4187		cart.delivery_method.extra_data = extraData;
4188		$cart.save();
4189	}
4190};
4191
4192$cart.hasEmail = function() {
4193	var cart = $cart.get();
4194	return cart && $utils.isEmail(cart.email);
4195};
4196
4197$cart.setEmail = function(email) {
4198	var cart = $cart.get();
4199	if (cart) {
4200		cart.email = email;
4201		$cart.save();
4202	}
4203};
4204
4205$cart.isCartItem = function(item, force) {
4206	if (item == null) {
4207		return false;
4208	}
4209	if (!$utils.isTrue(force) && ($entity.isGeoSchedule(item.entity) || !$instance.hasCart())) {
4210		return false;
4211	}
4212	if (!$entity.isGeoBuy(item.entity)) {
4213		return false;
4214	}
4215	if (item.geo_buy) {
4216		item = item.geo_buy;
4217	}
4218	return !$utils.isTrue($instance.parse(item.disabled)) && ($utils.isTrue(item.free) || item.price > 0);
4219};
4220
4221$cart.getStock = function(item) {
4222	if (item.geo_buy) {
4223		item = item.geo_buy;
4224	}
4225	var stock = item.places;
4226	if (isNaN(stock)) {
4227		return null;
4228	}
4229	if (item.purchases) {
4230		var p = $instance.parse(item.purchases);
4231		if (typeof p == 'number') {
4232			stock -= p;
4233		}
4234	}
4235	return stock;
4236};
4237
4238$cart.hasStock = function(o, extra) {
4239	var quantity = 1;
4240	if (extra) {
4241		quantity += extra;
4242	}
4243	var stock = $cart.getStock(o);
4244	//console.log('stock:'+stock + " vs quantity:"+quantity) 
4245	return stock == null || stock >= quantity;
4246};
4247
4248$cart.isUserRequiredToAddCart = function() {
4249	return $instance.current && $instance.current.geo_buy && $utils.isTrue($instance.current.geo_buy.user_required_to_add_cart);
4250};
4251
4252$cart.isAllowedAnonymous = function() {
4253	return $instance.current && $instance.current.geo_buy && $utils.isTrue($instance.current.geo_buy.allowed_anonymous);
4254};
4255
4256$cart.getAnonymousCartIdKey = function() {
4257	var instanceId = $instance.getId();
4258	if (instanceId) {
4259		return '_geo_anonymous_cart_' + instanceId + '_';
4260	}
4261	return null;
4262};
4263
4264$cart.getAnonymousCartId = function() {
4265	var key = $cart.getAnonymousCartIdKey();
4266	if (key) {
4267		return $storageService.get(key);
4268	}
4269	return null;
4270};
4271
4272$cart.setAnonymousCartId = function(id) {
4273	var key = $cart.getAnonymousCartIdKey();
4274	if (key) {
4275		if (id) {
4276			$storageService.put(key, id);
4277		} else {
4278			$storageService.remove(key);
4279		}
4280	}
4281};
4282
4283//// File media.js ////
4284
4285
4286var $media = {};
4287
4288$media.medias = {};
4289
4290/*
4291// problem passing img element in init function
4292$media.init = function(img, elem, mediaId) {
4293	img.onload = function() {
4294		$angular.onLoadImg(img);
4295	};
4296	img.onerror = function() {
4297		img.style.display='none';
4298	};
4299	if (elem != null) {
4300		var alt = $entity.displayColumn(elem);
4301		if (!$utils.isBlank(alt)) {
4302			$(img).prop("alt", alt);
4303        }
4304		if ($utils.isBlank(mediaId)) {
4305			mediaId = elem.geo_image;
4306		}
4307	}
4308	var url = $media.getMediaUrl(mediaId);
4309	if ($utils.isBlank(url)) {
4310		img.style.display='none';
4311		return;
4312	}
4313	console.log("Loading media: " + url);
4314	$(img).prop("src", url);
4315};
4316*/
4317
4318$media.getMediaInfo = function(mediaId) {
4319	var media = $media.medias[mediaId];
4320	if (media) {
4321		return media;
4322	}
4323	$rest.get({
4324		url: contextPath + ($constants.isWeb ? '/www/media/' : "/api/media/") + mediaId + '/info.json',
4325		showError: false
4326	}).then(function(o) {
4327		$media.medias[mediaId] = o ? o : {};
4328	}, function() {
4329		$media.medias[mediaId] = {};
4330	});
4331	return undefined;
4332};
4333
4334$media.getMediaAlt = function(mediaId) {
4335	var media = $media.getMediaInfo(mediaId);
4336	if (media === undefined) {
4337		return undefined;
4338	}
4339	if (media) {
4340		if (media.description && !$utils.isBlank(media.description)) {
4341			return media.description;
4342		}
4343		if (media.name && !$utils.isBlank(media.name)) {
4344			return media.name;
4345		}
4346	}
4347	return '';
4348};
4349
4350$media.getMediaUrl = function(item, extension) {
4351	if (item && item.extension === 'youtube') {
4352		return item.source_media_url;
4353	}
4354	var id = item;
4355	if (id) {
4356		if (id.image) {
4357			id = id.image;
4358		} else if (id.id) {
4359			id = id.id;
4360		}
4361	}
4362	if ($utils.isBlank(id)) {
4363		return "";
4364	}
4365	if (typeof id == 'string' && id.startsWith('http')) {
4366		return id;
4367	}
4368	if (extension == null) {
4369		extension = $angular.rootScope.imgExtension;
4370	}
4371	var url = $angular.rootScope.rootPath + ($constants.isWeb ? 'www' : 'api');
4372	return url + "/media/" + id + "." + extension + "?_l=" + $locale.language;
4373};
4374
4375$media.getMediaName = function(item) {
4376	if (item && item.name) {
4377		return item.name;
4378	}
4379	return item;
4380};
4381
4382$media.isYoutube = function(item) {
4383	if (typeof item == 'string') {
4384		return item.contains("youtube.com");
4385	}
4386	return item && item.extension === 'youtube';
4387}
4388
4389$media.getItemMediaUrl = function(entity, id) {
4390	return $angular.rootScope.rootPath + "www/geo/" + entity + "/" + id + "/media." + $angular.rootScope.imgExtension + "?_l=" + $locale.language;
4391};
4392
4393$media.openMedia = function(e, entity, id, type, column) {
4394	$utils.preventDefault(e);
4395	setTimeout(function() {
4396		$angular.openModal("panel-add-media", {entity: entity, id: id, type: type, column: column, size: 'xl'});
4397	}, 100);
4398};
4399
4400//// File user.js ////
4401var $user = {};
4402
4403$user.isInit = false;
4404
4405// vars TODO
4406$user.current = null;
4407//$user.isLogged = false;
4408//$user.isSuper = false;
4409//$user.isAdmin = false;
4410//$user.isBasicUser = false;
4411//$user.isLogged = false;
4412
4413$user.isLogged = function() {
4414	return $user.current != null;
4415};
4416
4417$user.showConsole = function() {
4418	return $user.current && $utils.isTrue($user.current.console);
4419};
4420
4421$user.set = function(callback) {
4422
4423	$rest.get({
4424		url: contextPath + "/api/logged.json",
4425		showError: false
4426	}).then(function(o) {
4427		//console.log("user logged in", o);
4428		$user.current = o;
4429		$angular.apply($angular.rootScope);
4430
4431		$cart.initCart();
4432
4433		if (callback) {
4434			callback();
4435			return;
4436		}
4437
4438		if (!$utils.isBlank($angular.tmpPath)) {
4439			var path = $angular.tmpPath;
4440			$angular.tmpPath = null;
4441			$angular.get("$location").path(path).replace();
4442			return;
4443		}
4444
4445		$angular.goHome();
4446
4447	}, function() {
4448		$user.current = null;
4449		if (callback) {
4450			callback();
4451		}
4452	});
4453
4454};
4455
4456$user.getRoleDesc = function() {
4457	if ($user.current) {
4458		var key = $constants.rolesList[$user.current.role];
4459		if (key) {
4460			return key;
4461		}
4462	}
4463	return "unknown";
4464};
4465
4466$user.isInstanceAllowed = function(i) {
4467	if ($user.isSuper()) {
4468		return true;
4469	}
4470	var u = $user.current;
4471	if (u && u.instances_allowed) {
4472		return u.instances_allowed.includes(i);
4473	}
4474	return false;
4475};
4476
4477$user.hasInstancesAllowed = function() {
4478	var u = $user.current;
4479	return u && u.instances_allowed && u.instances_allowed.length > 0;
4480};
4481
4482$user.isSuper = function() {
4483	return $user.hasRole('super');
4484};
4485$user.isAdmin = function() {
4486	return $user.hasRole('admin') || $user.isSuper();
4487};
4488$user.isCoordinator = function() {
4489	return $user.hasRole('coordinator');
4490};
4491$user.isBasicUser = function() {
4492	return $user.hasRole('basic');
4493};
4494$user.isPublic = function() {
4495	return $user.hasRole('public');
4496};
4497$user.isModeler = function() {
4498	return $user.isSuper() || ($user.current && $user.current.modeler);
4499};
4500
4501$user.hasRole = function(role) {
4502	return $user.current && $user.current.role === role;
4503};
4504
4505$user.openLoginAsPanel = function(callback) {
4506	$angular.openModal('panel-login').then(callback);
4507};
4508
4509$user.isAllowed = function(entity) {
4510	if ($user.isAdmin()) {
4511		return true;
4512	}	
4513	if ($user.isCoordinator()) {
4514		return (entity != null && entity.type == 'entity');
4515	}
4516	return false;
4517};
4518
4519//// File cookies.js ////
4520var $cookiesService = {};
4521
4522//The object may have following properties:
4523//- path - {string} - The cookie will be available only for this path and its sub-paths. By default, this is the URL that appears in your <base> tag.
4524//- domain - {string} - The cookie will be available only for this domain and its sub-domains. For security reasons the user agent will not accept the cookie if the current domain is not a sub-domain of this domain or equal to it.
4525//- expires - {string|Date} - String of the form "Wdy, DD Mon YYYY HH:MM:SS GMT" or a Date object indicating the exact date/time this cookie will expire.
4526//- secure - {boolean} - If true, then the cookie will only be available through a secured connection.
4527//Note: By default, the address that appears in your <base> tag will be used as the path. This is important so that cookies will be visible for all routes when html5mode is enabled.
4528	
4529$cookiesService.get = function(key) {
4530	if (document.cookie != null) {
4531		var nameEQ = key + "=";
4532  		var ca = document.cookie.split(';');
4533  		for (var i = 0; i < ca.length; i++) {
4534    		var c = ca[i];
4535    		while (c.charAt(0) === ' ') c = c.substring(1); // eliminar espacios
4536    		if (c.indexOf(nameEQ) === 0) {
4537      			return decodeURIComponent(c.substring(nameEQ.length));
4538			}
4539		}
4540	}
4541	return null;
4542};
4543
4544
4545$cookiesService.put = function(key, value, days) {
4546	if (value == null) {
4547		value = "";
4548	}
4549	if (days == null) {
4550		days = 0;
4551	}
4552	var seconds = days*24*60*60;
4553	var path = contextPath == "" ? "/" : contextPath;
4554	var secure = "; Secure; SameSite=Strict";
4555	var c = key + "=" + value + "; Max-Age=" + seconds + "; Path=" + path + secure;
4556	document.cookie = c;
4557	//console.log(c)
4558};
4559
4560$cookiesService.remove = function(key) {
4561	$cookiesService.put(key, "", 0);
4562};
4563
4564var $storageService = {};
4565
4566$storageService.get = function(key) {
4567	try {
4568		var $window = $angular.get("$window");
4569		var value = $window.localStorage[key];
4570		//console.log("get " + key + " : " + value)
4571		return value;
4572	} catch (e) {
4573		return $cookiesService.get(key);
4574	}
4575};
4576
4577$storageService.put = function(key, value) {
4578	try {
4579		var $window = $angular.get("$window");
4580		$window.localStorage[key] = value;
4581		//console.log("put " + key + " : " + value)
4582	} catch (e) {
4583		$cookiesService.put(key, value, 7);
4584	}
4585};
4586
4587$storageService.remove = function(key) {
4588	try {
4589		var $window = $angular.get("$window");
4590		$window.localStorage.removeItem(key);
4591		//console.log("remove " + key)
4592	} catch (e) {
4593		$cookiesService.remove(key);
4594	}
4595};
4596
4597var $sessionService = {};
4598
4599$sessionService.get = function(key) {
4600	try {
4601		var $window = $angular.get("$window");
4602		var value = $window.sessionStorage[key];
4603		//console.log("get " + key + " : " + value)
4604		return value;
4605	} catch (e) {
4606		return $cookiesService.get(key);
4607	}
4608};
4609
4610$sessionService.put = function(key, value) {
4611	try {
4612		var $window = $angular.get("$window");
4613		$window.sessionStorage[key] = value;
4614		//console.log("put " + key + " : " + value)
4615	} catch (e) {
4616		$cookiesService.put(key, value, 30);
4617	}
4618};
4619
4620$sessionService.remove = function(key) {
4621	try {
4622		var $window = $angular.get("$window");
4623		$window.sessionStorage.removeItem(key);
4624		//console.log("remove " + key)
4625	} catch (e) {
4626		$cookiesService.remove(key);
4627	}
4628};
4629
4630
4631//// File paginate.js ////
4632
4633
4634var $paginate = {};
4635var $paginator;
4636
4637$paginate.init = function(element, options) {
4638	$paginator = new $paginate.instance(element, options);
4639	
4640	if ($angular.contentScope) {
4641		$angular.contentScope.$paginator = $paginator;
4642	}
4643	
4644	if (options.noLoad) {
4645		// no init list;
4646		return;
4647	}
4648	// init list
4649	$paginator.updateList();
4650	
4651	return $paginator;
4652};
4653
4654$paginate.instance = function(element, options) {
4655	
4656	if (element == undefined) {
4657		throw "The element is required.";
4658	}
4659	if (options == undefined) {
4660		throw "The argument 'options' is required."; 
4661	}
4662	if (options.path == undefined) {
4663		throw "The parameter 'path' is required.";
4664	}
4665	
4666	var self = this;
4667	
4668	var _defaults = {
4669		numPerPage: 100,
4670		list: "list",
4671		scope: $angular.contentScope,
4672		sce: $angular.get("$sce"),
4673		indexMin: null,
4674		indexMax: null	
4675	}
4676	
4677	/* Public attributes */
4678	this._settings = $.extend( {}, _defaults, options );
4679	this._element = $(element);
4680	
4681	var limit = getParamNumber("_limit");
4682	if (limit > 0) {
4683		this._settings.numPerPage = Number(limit);
4684	}
4685	
4686	if (this._settings.queryString == null) {
4687		this._settings.queryString = getQueryString(window.location.href);
4688	}
4689	
4690	// fix urls params
4691	this._settings.queryString = removeParam(this._settings.queryString, "_limit");
4692	if (this._settings.numPerPage > 0) {
4693		this._settings.queryString = addParam(this._settings.queryString, "_limit", this._settings.numPerPage);
4694	}
4695	
4696	var index = getParamNumber("_index", this._settings.queryString);
4697	this._settings.queryString = removeParam(this._settings.queryString, "_index");
4698	this._settings.queryString = addParam(this._settings.queryString, "_index", index);
4699
4700	// add extra params
4701	if (options.params) {
4702		for (var key in options.params) {
4703			var value = options.params[key];
4704			if (!this._settings.queryString.contains(key)) {
4705				this._settings.queryString = addParam(this._settings.queryString, key, value);
4706			}
4707		}
4708	}
4709
4710	// Prev
4711	this._settings.scope.loadPrev = function(e, addAfter, addBefore) {
4712		$utils.preventDefault(e);
4713
4714		$("#load-prev a.btn").blur();
4715		if (addAfter == null) addAfter = false;
4716		if (addBefore == null) addBefore = true;
4717		$paginator.updateList(getPrevQueryString(), true, addAfter, addBefore);
4718	};
4719	
4720	// More
4721	this._settings.scope.loadMore = function(e, addAfter, addBefore) {
4722		$utils.preventDefault(e);
4723
4724		$("#load-more a.btn").blur();
4725		if (addAfter == null) addAfter = true;
4726		if (addBefore == null) addBefore = false;
4727		$paginator.updateList(getNextQueryString(), true, addAfter, addBefore);
4728	};
4729	
4730	/* Public methods */
4731	this.getQueryStringAll = function(params, withoutPaginationParams) {
4732		var qs = self._settings.queryString;
4733		qs = removeParam(qs, "_limit");
4734		if (withoutPaginationParams) {
4735			qs = removeParam(qs, "_paginate");
4736			qs = removeParam(qs, "_index");
4737			qs = removeParam(qs, "suffix");
4738		} else {
4739			qs = addParam(qs, "_paginate", "false");
4740			var id = $instance.getId();
4741			if (!$utils.isBlank(id)) {
4742				qs = addParam(qs, "geo_instance", id);
4743			}
4744		}
4745		if (params) {
4746			for (var key in params) {
4747				var param = params[key];
4748				qs = addParam(qs, param.key, param.value);
4749			}
4750		}
4751		return qs;
4752	};
4753	
4754	this.refreshList = function(force) {
4755		var qs = self._settings.queryString;
4756		qs = removeParam(qs, "_index");
4757		qs = addParam(qs, "_index", "0");
4758		this.updateList(qs, force);
4759	};
4760	
4761	this.firstTime = true;
4762	this.requesting = 0;
4763	
4764	this.updateList = function(queryString, force, addAfter, addBefore) {
4765		
4766		if (!$utils.isTrue(force) && self._settings.queryString == queryString) {
4767			return;
4768		}
4769
4770		if (queryString == undefined) {
4771			queryString = self._settings.queryString;
4772		}
4773		
4774		if ($user.isSuper()) {
4775			queryString = removeParam(queryString, "suffix");
4776			queryString = addParam(queryString, "suffix", "true");
4777		}
4778		
4779		var index = getParamNumber('_index', queryString);
4780		if (index == 0) {
4781			// reset range
4782			self._settings.indexMin = null;
4783			self._settings.indexMax = null;
4784		}
4785		
4786		var url = contextPath + "/"+this._settings.path+".json" + queryString;
4787		//console.log(url)
4788		self._settings.queryString = queryString;
4789		
4790		if (this.firstTime) {
4791			this.firstTime = false;
4792		} else {	
4793			// update current url for backs
4794			var params = queryStringToJSON(queryString);
4795			if (self._settings.numPerPage == 100) {
4796				delete params._limit;
4797			}
4798			delete params.suffix;
4799			//if (params._index == "0") {
4800			delete params._index;
4801			//}
4802			//console.dir(params)
4803			$angular.get("$location").search(params).replace();		
4804		}
4805		
4806		self.requesting++;
4807		var tmp = self.requesting;
4808		
4809		//var time = new Date().getTime();
4810		//console.log("1. " + url)
4811		$rest.get({
4812			skipHideLoading: true,
4813//			loading: loading,
4814			url: url
4815		}).then(function(data) {
4816			
4817			if (self.requesting != tmp) {
4818				console.log('skipping ' + url + " requesting:" + self.requesting + " tmp:" 
4818+ tmp)
4819				return;
4820			}
4821			
4822			//console.log("2. " + url)
4823			if (data && self._settings.scope.collapsed) {
4824				
4825				var l = [];
4826				var list = data[self._settings.list];
4827				if (list) {
4828					var colName = $entity.getDefaultColumnName($angular.contentScope.entity);
4829					var last = {};
4830					for (var i=0; i<list.length; i++) {
4831						var item = list[i];
4832						var name = item[colName];
4833						if (last[name] == null) {
4834							l.push(item);
4835							last[name] = item;
4836							last[name]._num = 0;
4837						}
4838						last[name]._num++;
4839					}
4840				}
4841				data[self._settings.list] = l;
4842				//console.log('is error!!! from '+ list.length + " to " + l.length)
4843			}
4844			
4845			if (addAfter) {
4846				
4847				// Append
4848				self._settings.scope.model[self._settings.list] = self._settings.scope.model[self._settings.list]
4849					.concat(data[self._settings.list]);
4850				
4851			} else if (addBefore) {
4852				
4853			    // Where the page is currently:
4854				var firstMsg = $('.list-row:first');
4855				var firstMsgId = firstMsg.attr("id");
4856				var offset = firstMsg.offset();
4857				if (offset && offset.top) {
4858					var curOffset = offset.top - $(document).scrollTop();
4859				    //console.log("top:"+offset.top + " - scrollTop:"+$(document).scrollTop())
4860
4861				    // Prepend
4862					self._settings.scope.model[self._settings.list] = data[self._settings.list]
4863						.concat(self._settings.scope.model[self._settings.list]);
4864					
4865					$angular.apply(self._settings.scope, function() {
4866					    // Offset to previous first message minus original offset/scroll
4867						firstMsg = $('#'+firstMsgId);
4868						//console.log("top:"+offset.top + " - curOffset:"+curOffset);
4869					    $(document).scrollTop(offset.top-curOffset);
4870					});
4871				}
4872				
4873			} else {
4874				
4875				// full list
4876				self._settings.scope.model = data;
4877				
4878				// clean cache
4879				if (data.list) {
4880					for (var k in data.list) {
4881						var o = data.list[k]
4882						if (o.id) {
4883							$constants.removeFromCache(null, o.id);
4884						}
4885					}
4886				}
4887			}
4888
4889			var index = getParamNumber('_index', url);
4890			
4891			// show pagination data
4892			if (index == 0) {
4893				if (data != null && data.count != null) {
4894					self._settings.numRows = data.count; 
4895				} else {
4896					self._settings.numRows = self._settings.scope.model[self._settings.list].length; 
4897				}
4898				showPagination();
4899			}	
4900			
4901			if (self._settings.indexMin == null || self._settings.indexMin > index) {
4902				self._settings.indexMin = index;
4903			}
4904			if (self._settings.indexMax == null || self._settings.indexMax < index) {
4905				self._settings.indexMax = index;
4906			}
4907			//console.log('min:'+self._settings.indexMin + ' max:'+self._settings.indexMax)
4908			//console.log('prev:'+getPrevIndex() + ' more:'+getNextIndex() + " size:" + self._settings.scope.model[self._settings.list].length)
4909			
4910			$angular.apply(self._settings.scope, function() {
4911				if (self._settings.numRows != null && getPrevIndex() >= 0) {
4912					$("#load-prev").removeClass("hidden");
4913				} else {
4914					$("#load-prev").addClass("hidden");
4915				}
4916				
4917				if (self._settings.numRows != null && getNextIndex() < self._settings.numRows) {
4918					$("#load-more").removeClass("hidden");
4919				} else {
4920					$("#load-more").addClass("hidden");
4921				}
4922				//console.log('load more visible ' + $("#load-more").is(":visible") + " - " + $("#load-more").hasClass("hidden") + " - " + $("#load-more").length)
4923			});
4924			
4925			
4926			$(window).unbind('scroll').scroll(function() {
4927				
4928				if ($(window).scrollTop() + $(window).height() > $(document).height() - 600) {
4929					if ($("#load-more").is(":visible")) {
4930						//console.log('load more')
4931						$(window).unbind('scroll');
4932						if ($("#load-more a.btn").length > 0) {
4933							$("#load-more a.btn").click();
4934						} else {
4935							$("#load-more").click();
4936						}
4937					}
4938					return;
4939				}
4940				
4941				if ($(window).scrollTop() == 0) {
4942					if ($("#load-prev").is(":visible")) {
4943						//console.log('load prev')
4944						$(window).unbind('scroll');
4945						if ($("#load-prev a.btn").length > 0) {
4946							$("#load-prev a.btn").click();
4947						} else {
4948							$("#load-prev").click();
4949						}
4950					}
4951					return;
4952				}
4953			});
4954			
4955			if (self._settings.onUpdated) {
4956				self._settings.onUpdated(data);
4957			}
4958			
4959		});
4960		
4961	}
4962
4963	function showPagination() {
4964		// show num results
4965		if (self._settings.numRows == null) {
4966			self._settings.numRows = 0;
4967		}
4968	    self._settings.scope.pagination_results = $utils.parseNumber(self._settings.numRows);
4969	}
4970	
4971	
4972	function getPrevQueryString() {
4973		var index = getPrevIndex();
4974		var queryString = self._settings.queryString;
4975		queryString = removeParam(queryString, "_index");
4976		queryString = addParam(queryString, "_index", index);
4977		return queryString;
4978	}
4979	function getPrevIndex() {
4980		return self._settings.indexMin - self._settings.numPerPage;
4981	}
4982	
4983	function getNextQueryString() {
4984		var index = getNextIndex();
4985		var queryString = self._settings.queryString;
4986		queryString = removeParam(queryString, "_index");
4987		queryString = addParam(queryString, "_index", index);
4988		return queryString;
4989	}
4990	function getNextIndex() {
4991		return self._settings.indexMax + self._settings.numPerPage;
4992	}
4993	
4994};
4995
4996// GENERIC METHODS TO MANAGE URL
4997function getQueryString(url) {
4998	if (url == undefined) {
4999		url = window.location.href;
5000	}
5001	var queryString = "";
5002	var split = url.split("?");
5003	if (split.length > 1 && split[1] != null) {
5004		queryString = split[1];
5005	}
5006	queryString = queryString.trim();
5007	if (queryString.length > 0) {
5008		queryString = "?" + queryString;
5009	}
5010	return queryString;
5011}
5012function getParam(name, url) {
5013	if (url == undefined) {
5014		url = window.location.href;
5015	}
5016	name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
5017	var regexS = "[\\?&]"+name+"=([^&#]*)";
5018	var regex = new RegExp( regexS );
5019	var results = regex.exec( url );
5020	if( results == null ) {
5021		return "";
5022	} else {
5023		return results[1];
5024	}
5025}
5026function getParamNumber(name, url) {
5027	var value = getParam(name, url);
5028	var orig = value;
5029	if ($utils.isBlank(value)) {
5030		value = 0;
5031	} else {
5032		value = Number(value);
5033		if (isNaN(value)) {
5034			value = 0;
5035		}
5036	}
5037	return value;
5038}
5039function addParam(url, param, value) {
5040	var sep = "&";
5041	if (url.indexOf("?") == -1) {
5042		sep = "?";
5043	}
5044	return url + sep + param + "=" + encodeURIComponent(value);
5045}
5046
5047function removeParam(url, param) {
5048	if (!url.contains(param)) {
5049		return url;
5050	}
5051	//console.log('1: remove ' + param + " " + url);
5052	
5053	//if (param.contains('$')) {
5054	param = param.replace('\$', '\\$');
5055	//}
5056	
5057	// remove all parameters starting with $
5058	var regex = new RegExp( "\\&" + param + "=[^&]*&?", "gi");
5059	var orig;
5060	do {
5061		orig = url;
5062		url = url.replace(regex,'&');
5063	} while (orig != url);
5064	
5065	// remove parameter starting with ?
5066	regex = new RegExp( "\\?" + param + "=[^&]*&?", "gi");
5067	url = url.replace(regex,'?');
5068	
5069	// remove if ends with ? or &
5070	url = url.replace(/(\?|&)$/,'');
5071	
5072	//console.log('2: remove ' + param + " " + url);
5073	return url;
5074}
5075
5076function queryStringToJSON(queryString) {
5077	if (queryString == null) {
5078		queryString = "";
5079	} else if (queryString.startsWith("?")) {
5080		queryString = queryString.substring(1);
5081	}
5082    var pairs = queryString.split('&');
5083    var result = {};
5084    pairs.forEach(function(pair) {
5085        pair = pair.split('=');
5086        if (!$utils.isBlank(pair[0])) {
5087            result[pair[0]] = decodeURIComponent(pair[1] || '');
5088	}
5089    });
5090    return JSON.parse(JSON.stringify(result));
5091}
5092
5093//// File instance.js ////
5094
5095
5096var $instance = {};
5097
5098$instance.current = null;
5099$instance.entities = null;
5100$instance.entityTitles = null;
5101$instance.languages = null;
5102
5103$instance.exists = function() {
5104	return $instance.current != null && $instance.current.name != null;
5105};
5106
5107$instance.getBaseUrl = function() {
5108	var id = $instance.getId();
5109	if (id == null && $constants.isWeb) {
5110		id = $instance.getIdToResolveInstance();
5111	}
5112	if (id != null) {
5113		if (!$constants.isWeb) {
5114			return "admin/i/" + id + "/";
5115		}
5116		if (!$utils.isBlank(appInstance) && appInstance == id) {
5117			return "";
5118		}
5119		return "home/" + id + "/";
5120	}
5121	if ($constants.isWeb) {
5122		console.log("warn: web baseurl to admin (no instance id)");		
5123	}
5124	return "admin/";
5125};
5126
5127$instance.exit = function() {
5128	$instance.current = null;
5129	$instance.languages = $constants.languages;
5130	//console.log('exit instance');
5131};
5132
5133$instance.hasToRefresh = false;
5134$instance.checkHasToRefresh = function(entity, id) {
5135	if (entity === "instance" || entity === "entity" 
5136			|| entity === "form" || entity === "category" 
5137			|| entity === "spreadsheet" || entity === "datamixer"
5138			|| entity === "synchronizer" || entity === "notifier") {
5139		$instance.hasToRefresh = true;
5140		
5141		if (entity === "instance" && id != null && id == $instance.getId()) {
5142			$instance.resolveInstance();
5143		}
5144	}
5145};
5146
5147$instance.resolveInstance = function(callback) {
5148	var id = $instance.getIdToResolveInstance();
5149	//var path = $angular.get("$location").path();
5150
5151	if ($instance.current && id == null) {
5152		// exit instance
5153		//console.log('exit instance ' + id + " " + path)
5154		$instance.exit();
5155		if ($user.isSuper()) {
5156			callback();
5157		} else {
5158			$user.set(function() {
5159				callback();
5160			});
5161		}
5162		return;
5163	}
5164
5165	var hasToRefresh = $instance.hasToRefresh;
5166	$instance.hasToRefresh = false;
5167
5168	if (id == null || (id == $instance.getId() && !hasToRefresh && $instance.exists())) {
5169		// no change
5170		//console.log('no change ' + id + " " + path, $instance.current)
5171		callback();
5172		return;
5173	}
5174	
5175
5176	// load instance id
5177	//console.log('load instance ' + id)
5178	$instance.set(id, callback);
5179};
5180
5181$instance.getId = function() {
5182	if ($instance.current) {
5183		return $instance.current.id;
5184	}
5185	return null;
5186};
5187
5188$instance.getIdToResolveInstance = function() {
5189	var path = $angular.get("$location").path();
5190	if (path) {
5191		if (path.startsWith("/admin/i/") || path.startsWith("/admin/instance/")) {
5192			var split = path.split("/");
5193			if (split.length > 3) {
5194				var id = split[3];
5195				if (id.length > 0 && id != 'new') {
5196					return id;
5197				}
5198			}
5199		}
5200		if (path.startsWith("/home/")) {
5201			var split = path.split("/");
5202			if (split.length > 2) {
5203				var id = split[2];
5204				if (id.length > 0) {
5205					return id;
5206				}
5207			}
5208		}
5209	}
5210	if (appInstance == "") {
5211		appInstance = null;
5212	}
5213	return appInstance;
5214};
5215
5216$instance.refresh = function() {
5217	$instance.set($instance.current.id);
5218};
5219
5220$instance.set = function(instanceId, callback) {
5221
5222	if ($utils.isBlank(instanceId)) {
5223		//$init.logError("instanceId required");
5224		return;
5225	}
5226	if (callback == null) {
5227		callback = function() {};
5228	}
5229	
5230	var loading = 'generic';
5231	var oldSkipHideLoading = $angular.rootScope.skipHideLoading;
5232	if (oldSkipHideLoading) {
5233		loading = null;
5234	}
5235
5236	var root = $constants.isWeb ? "/www" : "/api";
5237
5238	// load instance data
5239	$rest.get({
5240		url: contextPath + root + "/instance/" + instanceId + "/set.json",
5241		skipHideLoading: !$constants.isWeb,// false for errors in web
5242		instance: instanceId,
5243		loading: loading
5244	}).then(function(o) {
5245		//console.log('set instance', o)
5246		
5247		if ($constants.isWeb && $instance.current != null) {
5248			// update instance stylesheet
5249
5250			$("body").fadeOut(200, function() {
5251				$instance.endSet(o, callback, oldSkipHideLoading);
5252				
5253				// wait until css is loaded
5254				setTimeout(function() {
5255					$("body").fadeIn(200, function() {});
5256				}, 250);
5257			});
5258			return;
5259		}
5260		
5261		$instance.endSet(o, callback, oldSkipHideLoading);
5262		
5263	}, function(e) {
5264		$instance.current = null;
5265		$angular.rootScope.skipHideLoading = oldSkipHideLoading;
5266		callback();
5267
5268		if (e.response) {
5269			if (e.response.status == 0) {
5270				setTimeout(function() {
5271					$utils.showNoInternet();
5272				}, 100);
5273				return;
5274			}
5275			if (e.response.status == 403) {
5276				setTimeout(function() {
5277					$utils.showAccessDenied();
5278				}, 100);
5279				return;
5280			}
5281			if (e.response.status == 404) {
5282				setTimeout(function() {
5283					$utils.showNotFound();
5284				}, 100);
5285				return;
5286			}
5287		}
5288		setTimeout(function() {
5289			$utils.showError();
5290		}, 100);
5291	});
5292
5293};
5294
5295$instance.endSet = function(o, callback, oldSkipHideLoading) {
5296	$angular.rootScope.skipHideLoading = oldSkipHideLoading;
5297	$instance.current = o;
5298	$instance.entities = null;
5299	$instance.entityTitles = null;
5300	$cart.initCart();
5301	if ($instance.isTrackLocation()) {
5302		$angular.get('geolocationService').watchCurrentLocation();
5303	}
5304	if ($instance.current && $instance.current.name) {
5305		$("html > head > title").html($utils.parse($instance.current.name));
5306	}
5307	
5308	$instance.languages = o.languages;
5309	if ($instance.languages == null || $instance.languages.length == 0) {
5310		$instance.languages = $constants.languages;
5311	}
5312	
5313	$instance.mslogin = $utils.isTrue($utils.get($instance.current, "geo_login.mslogin"))
5314	
5315	if ($user.isSuper()) {
5316		callback();
5317	} else {
5318		$user.set(function() {
5319			callback();
5320		});
5321	}
5322};
5323
5324$instance.setCssProperties = function() {
5325	try {
5326		var background = getComputedStyle(document.documentElement).getPropertyValue('--color-background');
5327		if ($utils.isColorDark(background)) {
5328			$angular.mapStyle = $angular.mapDark;
5329		}
5330	} catch (e) {
5331		console.error(e);
5332	}
5333};
5334
5335$instance.getEntities = function(type) {
5336	var list = $instance.getEntityList(); 
5337	var result = [];
5338	for (var k in list) {
5339		var entity = list[k];
5340		if (entity.group == type || (type == 'settings' && $utils.isBlank(entity.group))) {
5341			if (!$user.isAllowed(entity)) {
5342				continue;
5343			}
5344			result.push(entity.id);
5345		}
5346	}
5347	return result;
5348};
5349
5350$instance.getEntityList = function() {
5351	var instance = $instance.current;
5352	if (instance && instance.entityList) {
5353		return instance.entityList;
5354	}
5355	return [];
5356};
5357
5358$instance.getCategoryName = function(id) {
5359	if ($instance.current && $instance.current.categoryList) {
5360		var list = $instance.current.categoryList;
5361		for (var k in list) {
5362			var cat = list[k];
5363			if (cat.id == id) {
5364				return $utils.parse(cat.name);
5365			}
5366		}
5367	}
5368	return null;
5369};
5370
5371// TODO use this instead of constants!
5372$instance.getCategoryList = function(entity) {
5373	var result = [];
5374	if ($instance.current && $instance.current.categoryList) {
5375		var list = $instance.current.categoryList;
5376		for (var k in list) {
5377			var cat = list[k];
5378			if (cat.entities && cat.entities.indexOf(entity) != -1) {
5379				result.push(cat);
5380			}
5381		}
5382	}
5383	return result;
5384};
5385
5386$instance.getSubInstances = function() {
5387	var instance = $instance.current;
5388	if (instance && instance.subInstances) {
5389		return instance.subInstances;
5390	}
5391	return [];
5392};
5393
5394$instance.getSubInstanceName = function(id) {
5395	var list = $instance.getSubInstances();
5396	for (var k in list) {
5397		var cat = list[k];
5398		if (cat.id == id) {
5399			return $utils.parse(cat.name);
5400		}
5401	}
5402	return null;
5403};
5404
5405$instance.getGeoSmarterText = function(key, msg) {
5406	if ($instance.current.geo_smarter == null) {
5407		$instance.current.geo_smarter = {};
5408	}
5409	var value = $instance.current.geo_smarter[key];
5410	if ($utils.isBlank(value) && !$utils.isBlank(msg)) {
5411		value = $angular.get("$translate").instant(msg);
5412	}
5413	return value;
5414};
5415
5416$instance.getGeoJson = function() {
5417	var instance = $instance.current;
5418	if (instance && instance.geo_fence) {
5419		return instance.geo_fence.geo_json;
5420	}
5421	return null;
5422};
5423
5424$instance.getGeoFence = function() {
5425	var instance = $instance.current;
5426	if (instance && instance.geo_fence) {
5427		return instance.geo_fence;
5428	}
5429	return null;
5430};
5431
5432$instance.getGeoFenceCenter = function() {
5433	var instance = $instance.current;
5434	if (instance && instance.geo_fence && instance.geo_fence.center_latitude) {
5435		var lat = Number(instance.geo_fence.center_latitude);
5436		var long = Number(instance.geo_fence.center_longitude);
5437		if (lat != 0 && long != 0) {
5438			return new google.maps.LatLng(lat, long);
5439		}
5440	}
5441	return null;
5442};
5443
5444$instance.getGeoPosition = function() {
5445	var latLng = $instance.getGeoFenceCenter();
5446	if (latLng) {
5447		return latLng.lat() + ',' + latLng.lng();
5448	}
5449	var instance = $instance.current;
5450	if (instance && instance.geo_location && instance.geo_location.latitude) {
5451		return instance.geo_location.latitude + ',' + instance.geo_location.longitude;
5452	}
5453	return null;
5454};
5455
5456$instance.getGeoFenceRadius = function() {
5457	var instance = $instance.current;
5458	if (instance && instance.geo_fence && instance.geo_fence.radius) {
5459		return Number(instance.geo_fence.radius);
5460	}
5461	return null;
5462};
5463
5464$instance.getGeoFenceZoom = function() {
5465	var instance = $instance.current;
5466	if (instance && instance.geo_fence && instance.geo_fence.default_zoom) {
5467		return Number(instance.geo_fence.default_zoom);
5468	}
5469	return null;
5470};
5471
5472$instance.getGeoFenceCountryCode = function() {
5473	var instance = $instance.current;
5474	if (instance && instance.geo_fence) {
5475		return instance.geo_fence.country_code;
5476	}
5477	return null;
5478};
5479
5480$instance.getGeoFenceBounds = function() {
5481	var instance = $instance.current;
5482	if (instance && instance.geo_fence && instance.geo_fence.bounds) {
5483		return instance.geo_fence.bounds;
5484	}
5485	var pos = $instance.getGeoFenceCenter();
5486	var radius = $instance.getGeoFenceRadius();
5487	if (pos && radius) {
5488		//console.log(pos.lat() + " " + pos.lng() + " " + radius)
5489//		instance.geo_fence.bounds = new google.maps.Circle({radius: radius, center: pos}).getBounds();
5490
5491		var bounds = new google.maps.LatLngBounds();
5492		var numPts = 128;
5493		for (var i = 0; i < numPts; i++) {
5494			var loc = google.maps.geometry.spherical.computeOffset(pos, radius, i * 360 / numPts);
5495			bounds.extend(loc);
5496		}
5497		instance.geo_fence.bounds = bounds;
5498		return instance.geo_fence.bounds;
5499	}
5500	return null;
5501};
5502
5503$instance.getSearchOptions = function() {
5504	var ops = {};
5505	var bounds = $instance.getGeoFenceBounds();
5506	if (bounds != null) {
5507		ops.bounds = bounds;
5508		ops.strictBounds = true;
5509	}
5510	var cc = $instance.getGeoFenceCountryCode();
5511	if (!$utils.isBlank(cc)) {
5512		ops.componentRestrictions = {country: cc};
5513	}
5514	return ops;
5515};
5516
5517$instance.getRecents = function() {
5518	var recents = $storageService.get("_recents_admin");
5519	if ($utils.isBlank(recents)) {
5520		return [];
5521	}
5522	return recents.split("||");
5523};
5524$instance.saveInRecents = function() {
5525	var id = $instance.getId();
5526	if ($utils.isBlank(id)) {
5527		return;
5528	}
5529	var list = $instance.getRecents();
5530	var index = list.indexOf(id);
5531	if (index == -1 && list.length > 7) {
5532		index = 7;
5533	}
5534	if (index != -1) {
5535		list.splice(index, 1);
5536	}
5537	list.unshift(id); // add first
5538	$instance.saveRecents(list);
5539};
5540$instance.saveRecents = function(list) {
5541	var recents = $utils.join(list, '||');
5542
5543	// save in recents TODO control not existing
5544	$storageService.put("_recents_admin", recents);
5545};
5546$instance.replaceInRecents = function(from, to) {
5547	var list = $instance.getRecents();
5548	var index = list.indexOf(from);
5549	if (index >= 0) {
5550		list[index] = to;
5551		$instance.saveRecents(list);
5552	}
5553};
5554
5555$instance.isProperty = function(name) {
5556	if ($instance.current && $instance.current.geo_properties) {
5557		var props = $instance.current.geo_properties;
5558		for (var k in props) {
5559			var prop = props[k];
5560			if (prop.name == name) {
5561				return prop.value == 'true';
5562			}
5563		}
5564	}
5565	return false;
5566};
5567
5568$instance.getEntityName = function(entity) {
5569	var o = $entity.getEntity(entity);
5570	if (o != null) {
5571		var value = $entity.displayColumn(o);
5572		if (value && o.name_extra && !$constants.isWeb && $user.isSuper()) {
5573			value += " (" + o.name_extra + ")";
5574		}
5575		return value;
5576	}
5577	return null;
5578};
5579
5580$instance.getEntity = function(entity) {
5581	if ($instance.entities == null) {
5582		$instance.entities = {};
5583		var l = $instance.getEntityList();
5584		for (var key in l) {
5585			var o = l[key];
5586			$instance.entities[o.id] = o;
5587		}
5588	}
5589	return $instance.entities[entity];
5590};
5591
5592$instance.hasEntity = function(entity) {
5593	return !!$instance.getEntity(entity);
5594};
5595
5596$instance.isEntity = function(entity) {
5597	var o = $entity.getEntity(entity);
5598	return o && o.type == "entity";
5599};
5600
5601$instance.getColor = function(path, def, def2) {
5602	var c = $utils.get($instance.current, path);
5603	if (c) {
5604		return $utils.getColor(c);
5605	} else if (def2) {
5606		return $instance.getColor(def, def2);
5607	} else {
5608		return def;
5609	}
5610};
5611
5612$instance.get = function(path, def) {
5613	var c = $utils.get($instance.current, path);
5614	if (c == null) {
5615		c = def;
5616	}
5617	//console.log("get: " + path+"="+c)
5618	return c;
5619};
5620
5621//function getBoundsByGeoJson(geojson) {
5622//	
5623//	if (typeof geojson === 'string') {
5624//		geojson = JSON.parse(geojson);
5625//	}
5626//	
5627//	var feature = {
5628//		type : "Feature",
5629//		geometry : geojson
5630//	};
5631//	//console.dir(feature)
5632//	
5633//	var bounds = new google.maps.LatLngBounds(null);
5634//	var data = new google.maps.Data();
5635//	data.addGeoJson(feature);	
5636//	data.forEach(function(feature) {
5637//		if (feature && feature.getGeometry()) {
5638//			feature.getGeometry().forEachLatLng(function(latLng) {
5639//				bounds.extend(latLng);
5640//			});
5641//		}
5642//	});
5643//	return bounds;
5644//}
5645
5646$instance.isBooking = function(item) {
5647	return item && item.geo_schedule && $entity.isGeoSchedule(item.entity)
5648		&& item.geo_schedule.booking && $utils.isTrue(item.geo_schedule.booking.enabled);
5649};
5650
5651$instance.isShowStock = function() {
5652	return $instance.current && $instance.current.geo_buy && $utils.isTrue($instance.current.geo_buy.show_stock);
5653};
5654
5655$instance.parse = function(p) {
5656	if (typeof p == 'object' && $instance.current) {
5657		p = p[$instance.current.id];
5658	}
5659	return p;
5660};
5661
5662$instance.hasCart = function() {
5663	return $instance.hasGeoBuy() && $instance.current && $instance.current.geo_buy 
5664		&& !$utils.isTrue($instance.parse($instance.current.geo_buy.disabled));
5665};
5666$instance.hasGeoBuy = function() {
5667	if ($instance.current) {
5668		return $utils.isTrue($instance.current.has_geo_buy);
5669	}
5670	return false;
5671};
5672$instance.hasGeoSchedule = function() {
5673	if ($instance.current) {
5674		return $utils.isTrue($instance.current.has_geo_schedule);
5675	}
5676	return false;
5677};
5678
5679$instance.hasAgenda = function() {
5680	if ($instance.current == null || $instance.current.geo_smarter == null || $utils.isTrue($instance.current.geo_smarter.disabled)) {
5681		return false;
5682	}
5683	var data = $sessionService.get("agenda_" + $instance.getId());
5684	if (data == null || $utils.isBlank(data)) {
5685		return false;
5686	}
5687	return true;
5688};
5689
5690$instance.getAgendaTitle = function() {
5691	return $instance.getGeoSmarterText("title_result", "msg.web.agenda");
5692};
5693
5694$instance.isLoginRequired = function(requiredLogged, publicUrl) {
5695	if (!$constants.isWeb) {
5696		// admin
5697		return $utils.isTrue(requiredLogged) && !$user.isLogged();
5698
5699	} else {
5700		// public
5701		if ($utils.isTrue(publicUrl)) {
5702			return false;
5703		}
5704		var required = $utils.isTrue(requiredLogged) ||
5705			($instance.exists() && $instance.current.geo_login && $utils.isTrue($instance.current.geo_login.required));
5706		if (required) {
5707			return !$user.isLogged() || $utils.isTrue($user.current.not_allowed);
5708		}
5709		return false;
5710	}
5711};
5712
5713$instance.isLoginDisabled = function() {
5714	if ($instance.current && $instance.current.geo_login) {
5715		var login = $instance.current.geo_login;
5716		if ($utils.isTrue(login.disabled)) {
5717			return true;
5718		}
5719	}
5720	return false;
5721};
5722
5723$instance.isSafeemployee = function() {
5724	if ($instance.current && $instance.current.products) {
5725		return $instance.current.products.includes('safeemployee');
5726	}
5727	return false;
5728};
5729
5730$instance.isTimeAndAttendance = function() {
5731	return $instance.current &&
5732		$instance.current.products &&
5733		$instance.current.products.includes('time-and-attendance');
5734};
5735
5736$instance.isTimeAndAttendanceTabAllowed = function(entityId) {
5737	return $instance.isTimeAndAttendance() &&
5738		(entityId === 'instance' || $instance.isAttendantsEntity(entityId) || $instance.isWorkdaysEntity(entityId));
5739}
5740
5741$instance.isAttendantsEntity = function(entityId) {
5742	return $instance.current && $instance.current.time_and_attendance &&
5743		$instance.current.time_and_attendance.attendants_entity === entityId;
5744};
5745
5746$instance.isWorkdaysEntity = function(entityId) {
5747	return $instance.current && $instance.current.time_and_attendance &&
5748		$instance.current.time_and_attendance.workdays_entity === entityId;
5749};
5750
5751$instance.isMicrosoftAccounts = function() {
5752	return $instance.current &&
5753		$instance.current.products &&
5754		$instance.current.products.includes('microsoft-accounts');
5755};
5756
5757//$instance.getFormViews = function(entity) {
5758//	if ($instance.current && $instance.current.formViews) {
5759//		var formViews = $instance.current.formViews[entity];
5760//		return formViews ? formViews : [];
5761//	}
5762//	return [];
5763//};
5764//$instance.getWebFormViews = function(entity) {
5765//	return $instance.getFormViews(entity).filter(function(form) {
5766//		return form.context ? form.context.includes('web') : false;
5767//	});
5768//};
5769//
5770//$instance.getWebCustomFormViews = function(entity) {
5771//	return $instance.getFormViews(entity).filter(function(form) {
5772//		return form.context ? form.context.includes('web_custom') : false;
5773//	});
5774//};
5775//
5776//$instance.getCreateFormViews = function(entity) {
5777//	return $instance.getFormViews(entity).filter(function(form) {
5778//		return form.context ? form.context.includes('backend_create') : false;
5779//	});
5780//};
5781//
5782//$instance.getUpdateFormViews = function(entity) {
5783//	return $instance.getFormViews(entity).filter(function(form) {
5784//		return form.context ? form.context.includes('backend_update') : false;
5785//	});
5786//};
5787
5788$instance.getSpreadsheets = function(entity, isList, download, upload, group) {
5789	var list = [];
5790	var spreadsheets = null;
5791	if ($instance.current == null) {
5792		if ($angular.contentScope.meta) {
5793			spreadsheets = $angular.contentScope.meta.spreadsheets;
5794		}
5795	} else {
5796		spreadsheets = $instance.current.spreadsheets;
5797	}
5798	if (spreadsheets != null) {
5799		for (var k in spreadsheets) {
5800			var item = spreadsheets[k];
5801			if (entity && item.entities && !item.entities.includes(entity)) {
5802				continue;
5803			}
5804			if (download && !item.download) {
5805				continue;
5806			}
5807			if (upload && !item.upload) {
5808				continue;
5809			}
5810			if (group && item.show_on_group_home !== group) {
5811				continue;
5812			}
5813			//console.log(item);
5814			if (isList && ($utils.isTrue(item.is_list) || $utils.isTrue(item.bulk_download_allowed))) {
5815				list.push(item);
5816			} else if (!isList && !$utils.isTrue(item.is_list)) {
5817				list.push(item);
5818			}
5819		}
5820	}
5821	return list;
5822};
5823
5824$instance.getNotifiers = function(entities) {
5825	if (entities && $instance.current && $instance.current.notifiers) {
5826		if(!Array.isArray(entities)) {
5827			entities = [entities];
5828		}
5829		return $instance.current.notifiers.filter(function(notifier) {
5830			return entities.indexOf(notifier.object_entity) >= 0;
5831		});
5832	}
5833	return [];
5834};
5835
5836$instance.getTriggers = function(entity) {
5837	if (entity && $instance.current && $instance.current.triggers) {
5838		return $instance.current.triggers.filter(function(trigger) {
5839			return trigger.events && trigger.events.map(function(event) {
5840				return event.object_entity;
5841			}).indexOf(entity) >= 0;
5842		});
5843	}
5844	return [];
5845};
5846
5847$instance.getButtonTriggers = function() {
5848	if ($instance.current && $instance.current.buttonTriggers) {
5849		var entity = $angular.contentScope.entityId;
5850		if ($instance.current.buttonTriggers[entity]) {
5851			return $instance.current.buttonTriggers[entity];
5852		}
5853	}
5854	return [];
5855};
5856
5857$instance.getButtonTrigger = function(code) {
5858	if (code) {
5859		var list = $instance.getButtonTriggers();
5860		if (list) {
5861			for (var k in list) {
5862				if (list[k].code == code) {
5863					return list[k];
5864				}
5865			}
5866		}
5867	}
5868	return null;
5869};
5870
5871$instance.getDatamixers = function(group) {
5872	if ($instance.current && $instance.current.datamixers) {
5873		return $instance.current.datamixers.filter(function(datamixer) {
5874			return !group || datamixer.show_on_group_home === group;
5875		});
5876	}
5877	return [];
5878};
5879
5880$instance.isPublic = function() {
5881	return $utils.isTrue($utils.get($instance.current, "public"));
5882};
5883
5884$instance.isTrackLocation = function() {
5885	return $constants.isWeb && $utils.isTrue($utils.get($instance.current, 'track_location'));
5886};
5887
5888$instance.mslogin = false;
5889$instance.isMslogin = function() {
5890	return $constants.isWeb && $instance.mslogin;
5891};
5892
5893
5894//// File angular.js ////
5895var $angular = {};
5896
5897$angular.app = null;
5898$angular.factories = {};
5899$angular.controllers = {};
5900$angular.components = {};
5901$angular.title = "";
5902$angular.rootScope = {};
5903$angular.headerScope = {};
5904$angular.menuScope = {};
5905$angular.contentScope = {};
5906$angular.customController = {};
5907$angular.tz = 0;
5908
5909$angular.starting = false;
5910$angular.load = function() {
5911	if ($angular.starting) {
5912		//$init.log("load already started");
5913		return;
5914	}
5915	$angular.starting = true;
5916	try {
5917		$init.log("start app");
5918		startApp();
5919		$init.log("end app");
5920	} catch (e) {
5921		$init.logError(e);
5922	}
5923};
5924
5925$angular.log = function(msg, a, b, c) {
5926	$init.logError(msg);
5927	if (a) console.log('o1', a);
5928	if (b) console.log('o2', b);
5929	if (c) console.log('o3', c);
5930
5931	if ($angular.consoleScope) {
5932		$angular.consoleScope.addLog(msg);
5933	}
5934};
5935
5936//get angular instance
5937$angular.injector = null;
5938$angular.get = function(name) {
5939	if ($angular.injector == null) {
5940		$angular.injector = angular.element(document.body).injector();
5941		if ($angular.injector == null) {
5942			return null;
5943		}
5944	}
5945	return $angular.injector.get(name);
5946};
5947
5948$angular.searchInScope = function(name) {
5949	var elem = null;
5950	$('.ng-scope').each(function(){
5951	    var s = angular.element(this).scope();
5952	    var o = s[name];
5953		if (o != null) {
5954	        elem = o;
5955	        return false; // stop looking at the rest
5956	    }
5957	});
5958	return elem;
5959};
5960
5961$angular.element = function(sel) {
5962	return angular.element(document.querySelectorAll(sel));
5963};
5964
5965$angular.apply = function($scope, callback) {
5966	$angular.apply2($scope, callback, 0, 0);
5967};
5968$angular.apply2 = function($scope, callback, tries1, tries2) {
5969	try {
5970		if ($scope && $scope.$apply) {
5971			if ($scope.$$phase) { // controlling digest in progress
5972				if (tries1 == 10) {
5973					$init.logError("Stop apply due to in progress");
5974					return;
5975				}
5976				tries1++;
5977				setTimeout(function() {
5978					$angular.apply2($scope, callback, tries1, tries2);
5979				}, 50);
5980				return;
5981			}
5982			$scope.$apply(function() {
5983				//$events.updateLastScroll();
5984				//console.log('good! ' + tries1 + " " + tries2)
5985				if (callback) {
5986					try {
5987						callback();
5988					} catch (e) {
5989						$init.logError(e);	
5990					}
5991				}
5992			});
5993		}
5994	} catch (e) {
5995		if (tries2 == 3) {
5996			$init.logError(e);
5997			return;
5998		}
5999		tries2++;
6000		setTimeout(function() {
6001			$angular.apply2($scope, callback, tries1, tries2);
6002		}, 50);
6003	}
6004};
6005
6006$angular.onLoadImg = function(elem) {
6007	var w = elem.naturalWidth;
6008	var h = elem.naturalHeight;
6009	if (h > w) {
6010		$(elem).addClass("portrait");
6011	}
6012	$angular.upgradeImg(elem);
6013};
6014$angular.onLoadImgLandscape = function(elem) {
6015	var w = elem.naturalWidth;
6016	var h = elem.naturalHeight;
6017	var ratio = w / h;
6018	if (ratio < 1.1) {
6019		$(elem).addClass("portrait");
6020	}
6021	$angular.upgradeImg(elem);
6022};
6023$angular.upgradeImg = function(elem) {
6024	if (document.body.classList.contains('is-scrolling')) {
6025		setTimeout(function() {
6026			$angular.upgradeImg(elem);
6027		}, 250);
6028		return;
6029	}
6030	var href = $(elem).prop("src");
6031	if ($utils.isBlank(href)) {
6032		return;
6033	}
6034	if (href.contains("."+$angular.imgExtensionFinal)) {
6035		return;
6036	}
6037	if (href.contains("."+$angular.imgExtensionImg)) {
6038		href = href.replace("."+$angular.imgExtensionImg, "."+$angular.imgExtensionFinal);
6039		$(elem).prop("src", href);
6040		return;
6041	}
6042	if (href.contains("."+$angular.imgExtension)) {
6043		href = href.replace("."+$angular.imgExtension, "."+$angular.imgExtensionImg);
6044		$(elem).prop("src", href);
6045		return;
6046	}
6047};
6048
6049$angular.start = function() {
6050	angular.bootstrap(document.getElementsByTagName('html')[0], ["myApp"]);
6051};
6052
6053$angular.init = function() {
6054
6055	// init app
6056	$angular.app = angular.module("myApp", $angular.modules);
6057
6058	$angular.tz = $utils.getTimeZoneOffset();
6059	$instance.languages = $constants.languages;
6060	//console.dir($angular.tz)
6061
6062	// config requirejs to load js dynamically
6063	require.config({
6064		baseUrl: contextPath,
6065		urlArgs: function(id, url) {
6066			if (url.indexOf('https') > -1) {
6067				return '';
6068			}
6069			return url;
6070		},
6071		waitSeconds: 200,
6072		paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs' }
6073	});
6074	
6075	// currencies
6076	$angular.currencies = $i18n.currencies;
6077
6078	$angular.app.config(function($provide, $routeProvider, $locationProvider,
6079		$translateProvider, $controllerProvider, $qProvider, $sceProvider) {
6080		
6081		try {
6082			$init.log("start config");
6083
6084			// $compileProvider.debugInfoEnabled(false); TODO no work
6085			$qProvider.errorOnUnhandledRejections(true);
6086			$sceProvider.enabled(false); // all html is trusted :)
6087
6088			$provide.decorator("$exceptionHandler", function() {
6089				return function(exception, cause) {
6090					$init.logError(exception);
6091				};
6092			});
6093
6094			// init route
6095			$angular.prepareRouteProvider($routeProvider);
6096
6097			// to use url without '#'
6098			$locationProvider.html5Mode(true);
6099
6100			// init i18n
6101			var urlLang = getParam("_lang");
6102			if (!$utils.isBlank(urlLang)) {
6103				// check request param _lang
6104				$locale.language = urlLang;
6105				$cookiesService.put("_language", $locale.language, 90);
6106			} else {
6107				// check cookie
6108				var clang = $cookiesService.get("_language");
6109				if (!$utils.isBlank(clang)) {
6110					$locale.language = clang;
6111				}
6112			}
6113			$locale.default_lang = "en";
6114			if ($locale.language == "") {
6115				$locale.language = $locale.default_lang;
6116			}
6117			var availables = {
6118				'*': $locale.default_lang
6119			}
6120			for (var key in $constants.languages) {
6121				var lang = $constants.languages[key];
6122				availables[lang + '*'] = lang;
6123			}
6124			for (var lng in $i18n.messages) {
6125				$translateProvider.translations(lng, $i18n.messages[lng]);
6126			}
6127			$translateProvider
6128				.useSanitizeValueStrategy('escapeParameters')
6129				.registerAvailableLanguageKeys($constants.languages, availables)
6130				.fallbackLanguage($locale.default_lang)
6131				.preferredLanguage($locale.language);
6132				
6133				
6134			// improve scroll without blinks
6135			window.addEventListener('wheel', function () {
6136				// do nothing
6137			}, { passive: true });
6138
6139			// Disable pointer events during scroll to optimize rendering performance
6140			(function() {
6141				var scrollTimeout;
6142				window.addEventListener('scroll', function () {
6143					if (!document.body.classList.contains('is-scrolling')) {
6144						document.body.classList.add('is-scrolling');
6145					}
6146					clearTimeout(scrollTimeout);
6147					scrollTimeout = setTimeout(function () {
6148						document.body.classList.remove('is-scrolling');
6149					}, 150);
6150				}, { passive: true });
6151			})();
6152
6153			$init.log("end config");
6154
6155		} catch (e) {
6156			// catch errors manually here
6157			$init.logError(e);
6158		}
6159
6160	});
6161
6162	if ($angular.modules.indexOf('ngTagsInput') != -1) {
6163		$angular.app.config(function(tagsInputConfigProvider) {
6164			$angular.tagsInputConfigProvider = tagsInputConfigProvider;
6165		});
6166	}
6167
6168	for (var name in $angular.factories) {
6169		$angular.app.factory(name, $angular.factories[name]);
6170	}
6171	for (var name in $angular.controllers) {
6172		$angular.app.controller(name, $angular.controllers[name]);
6173	}
6174	for (var name in $angular.components) {
6175		$angular.app.component(name, $angular.components[name]);
6176	}
6177
6178	$angular.app.controller('console', function($rootScope, $scope, $sce, $timeout) {
6179		$angular.consoleScope = $scope;
6180		$scope.showConsole = false;
6181		$scope.consoleContent = "";
6182		$scope.consoleList = "";
6183		$timeout(function() {
6184			$("#console").removeClass("hidden");
6185		});
6186		$scope.addLog = function(o) {
6187			//console.log('log', o)
6188			if (o == null) {
6189				return;
6190			}
6191			if (!$user.showConsole()) {
6192				return;
6193			}
6194			var show = false;
6195			if (typeof o == 'string') {
6196//				var date = new Date().getTime();
6197//				var msg = $utils.parseDateTotal(date) + ' ERROR ' + o;
6198				var msg = o; 
6199				console.warn(msg);
6200				$scope.consoleList = '<li class="WARN">' + msg + "</li>" + $scope.consoleList;
6201				show = true;
6202			}
6203			if ($utils.isArray(o)) {
6204				for (var x in o.reverse()) {
6205					var item = o[x];
6206					//var date = item.date; //new Date().getTime();
6207					if ($scope.consoleList.contains(item.msg)) {
6208						continue;
6209					}
6210					//var msg = $utils.parseDateTotal(date) + ' ' + item.level + ' ' + item.msg;
6211					var msg = item.msg;
6212					console.warn(msg);
6213					$scope.consoleList = '<li class="' + item.level + '">' + msg + "</li>" + $scope.consoleList;
6214					if (item.level != 'INFO') {
6215						show = true;
6216					}
6217				}
6218			}
6219			$scope.consoleContent = $sce.trustAsHtml(
6220				'<span class="close material-icons" onclick="$angular.clearConsole()">close</span> '
6221				+ '<div class="panel panel-default"><div class="panel-body"><ul>' + $scope.consoleList + '</ul></div></div>');
6222			if (!$scope.showConsole) {
6223				$scope.showConsole = true;
6224				$angular.apply($scope, function() {
6225					$("#console").click();						
6226				});
6227			}
6228		};
6229	});
6230	$angular.clearConsole = function() {
6231		// $("#console").click();
6232		$angular.consoleScope.showConsole = false;
6233		$angular.consoleScope.consoleContent = "";
6234		$angular.consoleScope.consoleList = ""; // clear logs
6235		$angular.apply($angular.consoleScope);
6236	};
6237
6238	$angular.app.directive('iframeSrc', function() {
6239		return {
6240			link: function(scope, element, attrs) {
6241				//console.log('iframeSrc ' + attrs.iframeSrc)
6242				element[0].src = $angular.get('$sce').trustAsResourceUrl(attrs.iframeSrc);
6243			}
6244		};
6245	});
6246
6247	$angular.app.directive("autopath", function() {
6248		return {
6249			restrict: 'A',
6250			require: 'ngModel',
6251			scope: { entity: '@', innerEntity: '@', autopath: '=' },
6252			link: function(scope, element, iAttrs, ngModelCtrl) {
6253				var _auto = null;
6254				var _source = null;
6255				element.bind('focus', function() {
6256					var source;
6257					if (scope.autopath === false) {
6258						source = [];
6259					} else if ($utils.isArray(scope.autopath)) {
6260						source = scope.autopath;
6261					} else {
6262						source = contextPath + "/api/autopath.json?entity=" + scope.entity;
6263						var innerEntity = scope.innerEntity;
6264						if (innerEntity) {
6265							source += "&innerEntity=" + innerEntity;
6266						}
6267					}
6268					if (source !== _source) {
6269						_source = source;
6270						_auto = element.autocomplete({
6271							classes: { "ui-autocomplete": "zoom" },
6272							minLength: 0,
6273							source: source,
6274							select: function(event, ui) {
6275								ngModelCtrl.$setViewValue(ui.item.value);
6276							}
6277						});
6278					}
6279
6280					// force key down to open
6281					var e = jQuery.Event("keydown");
6282					e.which = 40;
6283					_auto.trigger(e);
6284				});
6285			}
6286		}
6287	});
6288
6289	$angular.app.directive("autoclasses", function() {
6290		return {
6291			restrict: 'A',
6292			require: 'ngModel',
6293			link: function(scope, element, iAttrs) {
6294				element.autocomplete({
6295					classes: { "ui-autocomplete": "zoom" },
6296					minLength: 0,
6297					source: function(request, response) {
6298						response(["group-bar", "group-card", "button-centered"]);
6299					}
6300				}).focus(function() {
6301					$(this).autocomplete("search");
6302				});
6303			}
6304		}
6305	});
6306
6307	$angular.app.directive("extSubmit", ['$timeout', function($timeout) {
6308		return {
6309			link: function($scope, $el, $attr) {
6310				$scope.$on('makeSubmit', function(event, data) {
6311					if (data.formName === $attr.name) {
6312						$timeout(function() {
6313							$el.triggerHandler('submit');
6314						}, 0, false);
6315					}
6316				})
6317			}
6318		};
6319	}]);
6320
6321	// ui-select - refresh-on-active
6322	$angular.app.directive('refreshOnActive', function() {
6323		return {
6324			restrict: 'A',
6325			link: refreshOnActiveLink,
6326			scope: {
6327				refreshOnActive: '=',
6328				refresh: '@'
6329			}
6330		}
6331
6332		function refreshOnActiveLink(scope, element) {
6333			if (!scope.refreshOnActive === true) {
6334				return;
6335			}
6336			var storedFunction = scope.$parent.$select.refresh;
6337			scope.$parent.$select.refresh = function() { };
6338
6339			var element = angular.element(element).closest('.ui-select-container');
6340			var fn = function() {
6341				scope.$parent.$select.refresh = function() {
6342					storedFunction(scope.refresh);
6343				};
6344				scope.$parent.$select.refresh.call();
6345				element.unbind('click', fn);
6346			};
6347			element.bind('click', fn);
6348		}
6349	});
6350
6351	$angular.app.directive('inputNumber', function() {
6352		return {
6353			require: 'ngModel',
6354			link: function(scope, element, attrs, ngModel) {
6355				ngModel.$formatters.push(function(value) {
6356					if (value) {
6357						if (typeof value == 'number') {
6358							return value;
6359						}
6360						if ($utils.isArray(value) && value.length == 1) {
6361							value = value[0];
6362						}
6363						if (typeof value == 'number') {
6364							return value;
6365						}
6366						if (typeof value == 'string') {
6367							return parseFloat(value);
6368						}
6369						$angular.log("Not number: " + JSON.stringify(value));
6370						return null;
6371					}
6372					return value;
6373				});
6374			}
6375		};
6376	});
6377
6378	$angular.app.directive('inputDatetime', function() {
6379		return {
6380			require: 'ngModel',
6381			link: function(scope, element, attrs, ngModel) {
6382				ngModel.$parsers.push(function(value) {
6383					//console.log('datetime parse', value) OUT
6384					if (value == null) {
6385						return null;
6386					}
6387					var val = value.getTime();
6388					return val;
6389				});
6390				ngModel.$formatters.push(function(value) {
6391					//console.log('datetime format', value) IN
6392					if (value == null) {
6393						return null;
6394					}
6395					var d = $utils.toDate(value);
6396					if (d != null) {
6397						d.setSeconds(0,0);
6398					}
6399					return d;
6400				});
6401			}
6402		};
6403	});
6404	$angular.app.directive('inputDate', function() {
6405		return {
6406			require: 'ngModel',
6407			link: function(scope, element, attrs, ngModel) {
6408				ngModel.$parsers.push(function(value) {
6409					return $utils.toISODay(value);
6410				});
6411				ngModel.$formatters.push(function(value) {
6412					return $utils.parseDay(value);
6413				});
6414			}
6415		};
6416	});
6417
6418	$angular.app.directive('restrictTo', function() {
6419		return {
6420			restrict: 'A',
6421			link: function(scope, element, attrs) {
6422				var re = RegExp(attrs.restrictTo);
6423				var exclude = /Backspace|Enter|Tab|Delete|Del|ArrowUp|Up|ArrowDown|Down|ArrowLeft|Left|ArrowRight|Right/;
6424
6425				element[0].addEventListener('keydown', function(event) {
6426					if (!exclude.test(event.key) && !re.test(event.key)) {
6427						event.preventDefault();
6428					}
6429				});
6430			}
6431		}
6432	});
6433
6434	$angular.app.directive("ngUploadChange", function() {
6435		return {
6436			scope: {
6437				ngUploadChange: "&"
6438			},
6439			link: function($scope, $element) {
6440				$element.on("change", function(event) {
6441					$angular.apply($scope, function() {
6442						$scope.ngUploadChange({ $event: event });
6443					});
6444				})
6445				$scope.$on("$destroy", function() {
6446					$element.off();
6447				});
6448			}
6449		}
6450	});
6451	
6452	$angular.app.directive("monacoEditor", function() {
6453	    return {
6454	        restrict: 'E',
6455	        scope: {
6456	            code: '=',
6457	            language: '@',
6458	            theme: '@'
6459	        },
6460	        link: function (scope, element) {
6461
6462				require(["vs/editor/editor.main"], function () {
6463    				//console.log('Loaded monaco', monaco)
6464    			
6465	                var editor = monaco.editor.create(element[0], {
6466	                    value: scope.code || '',
6467	                    language: scope.language,
6468        				theme: "vs-dark",
6469        				automaticLayout: true
6470	                });
6471		
6472	                // Update model on editor change
6473	                editor.onDidChangeModelContent(function() {
6474						scope.$apply(function() {
6475						    scope.code = editor.getValue();
6476						});
6477	                });
6478		
6479	                // Watch for external changes to `code`
6480	                scope.$watch('code', function(newValue) {
6481	                    if (newValue !== editor.getValue()) {
6482	                        editor.setValue(newValue || '');
6483	                    }
6484	                });
6485	                
6486				});	               
6487	        }
6488	    };
6489	});
6490	
6491	$angular.app.directive('encryptedField', function() {
6492	  return {
6493	    restrict: 'E',
6494	    scope: {
6495	      model: '=',
6496	      field: '='
6497	    },
6498	    template: `
6499	      <input type="{{inputType}}"
6500	             ng-model="inputValue"
6501	             ng-change="handleChange()"
6502	             class="form-control" />
6503	    `,
6504	    link: function(scope) {
6505	      const ENCRYPTED_PREFIX = '{enc}';
6506	      const MASK = '****************';
6507	
6508	      const originalValue = scope.model != null ? scope.model[scope.field] : null;
6509	      const isEncrypted = originalValue && originalValue.startsWith(ENCRYPTED_PREFIX);
6510	
6511	      scope.inputType = isEncrypted ? 'password' : 'text';
6512	      scope.inputValue = isEncrypted ? MASK : '';
6513	
6514	      let wasModified = false;
6515	      scope.handleChange = function() {
6516	      	if (isEncrypted && !wasModified && scope.inputValue !== MASK) {
6517	        	scope.inputValue = ''; // limpiar si se modifica cualquier carácter
6518	          	scope.inputType = 'text'; // mostrar texto plano
6519	          	wasModified = true;
6520	      	}
6521	      	scope.model[scope.field] = scope.inputValue;
6522	      };
6523	    }
6524	  };
6525	});
6526
6527
6528
6529	$angular.app.animation('.reveal-animation', function() {
6530		return {
6531			enter: function(element, done) {
6532				element.css('display', 'none');
6533				element.fadeIn(200, done);
6534				//element.show();
6535				return function() {
6536					element.stop();
6537				}
6538			},
6539			leave: function(element, done) {
6540				element.hide();//fadeOut(500, done)
6541				element.remove();
6542				return function() {
6543					element.stop();
6544				}
6545			}
6546		}
6547	});
6548	
6549}
6550
6551$angular.prepareComponent = function(self, $scope, $timeout) {
6552
6553	// to access root scope vars
6554	var rs = $angular.rootScope;
6555	for (var k in rs) {
6556		if (k.startsWith("$")) {
6557			continue;
6558		}
6559		$scope[k] = rs[k];
6560		//console.log(k, $scope[k])
6561	}
6562	$scope.$utils = $utils;
6563	$scope.$angular = $angular;
6564	$scope.$constants = $constants;
6565	$scope.$user = $user;
6566	$scope.$entity = $entity;
6567	$scope.$instance = $instance;
6568	$scope.$locale = $locale;
6569	$scope.$events = $events;
6570	$scope.$media = $media;
6571	$scope.$cart = $cart;
6572
6573	$timeout(function() {
6574		$scope.refresh();
6575	});
6576
6577	return self;
6578}
6579
6580$angular.initRootScope = function($rootScope, $location, $window, $timeout, isWeb) {
6581
6582	$angular.rootScope = $rootScope;
6583
6584	// constants
6585	$constants.isDev = location.host.indexOf('localhost') !== -1;
6586	$constants.isWeb = isWeb;
6587	$constants.isPro = isPro;
6588	$constants.isCustomDomain = isCustomDomain;
6589
6590	$angular.rootScope.$utils = $utils;
6591	$angular.rootScope.$angular = $angular;
6592	$angular.rootScope.$constants = $constants;
6593	$angular.rootScope.$user = $user;
6594	$angular.rootScope.$entity = $entity;
6595	$angular.rootScope.$instance = $instance;
6596	$angular.rootScope.$locale = $locale;
6597	$angular.rootScope.$events = $events;
6598	$angular.rootScope.$media = $media;
6599	if (typeof $cart != 'undefined') {
6600		$angular.rootScope.$cart = $cart;
6601	}
6602
6603	$angular.rootScope.contextPath = contextPath;
6604	$angular.rootScope.rootPath = rootPath;
6605	
6606	$angular.initMap();
6607	$angular.initServiceWorker();
6608
6609	// TODO improve image load
6610	$rootScope.imgExtension = "thumb";
6611	$rootScope.imgExtensionImg = "img";
6612	$rootScope.imgExtensionFinal = "big";
6613
6614	$angular.imgExtension = $rootScope.imgExtension;
6615	$angular.imgExtensionImg = $rootScope.imgExtensionImg;
6616	$angular.imgExtensionFinal = $rootScope.imgExtensionFinal;
6617
6618	// init firebase
6619	if (typeof firebase != 'undefined') {
6620		firebase.initializeApp({
6621			apiKey: "AIzaSyDiSBSk5BN9XX1uILUQ6KiEylso1xKr4DA",
6622			authDomain: "geo-we.firebaseapp.com",
6623			databaseURL: "https://geo-we.firebaseio.com",
6624			projectId: "geo-we",
6625			storageBucket: "geo-we.appspot.com",
6626			messagingSenderId: "372903613421",
6627			appId: "1:372903613421:web:08c3174e8c857f08"
6628		});
6629	}
6630
6631//	$rootScope.$on('$translateChangeSuccess', function() {
6632//	});
6633
6634	$rootScope.$on('$routeChangeError', function(event, next, previous, error) {
6635		$angular.log(error);
6636	});
6637
6638	$angular.isBackNavigation = false; 
6639	$window.addEventListener('popstate', function() { 
6640		$angular.isBackNavigation = true; 
6641		//console.log('is back navigation')
6642	});
6643	$angular.positions = {};
6644	$rootScope.$on('$locationChangeStart', function(a,b,c) {
6645		var name = c;
6646		var pos = $window.pageYOffset;
6647		$angular.positions[name] = pos;
6648		$angular.ready = false;
6649		//console.log("save: " + name + " = " + pos);
6650	});
6651
6652	$angular.openLicense = function(key, showButtons) {
6653		$angular.openModal("panel-licenses", { key:key, showButtons:showButtons, size:'lg' });
6654	};
6655
6656	$rootScope.getCurrentPath = function() {
6657		return contextPath + $angular.get('$location').path();
6658	};
6659
6660	// fullscreen
6661	$rootScope.clickImage = function(e, list, index) {
6662		$utils.preventDefault(e);
6663		//		if (typeof index != 'number' && index.$parent) {
6664		//			index = index.$parent.carousel.getCurrentIndex();
6665		//		}
6666		$rootScope.fsList = list;
6667		$rootScope.fsIndex = index;
6668		$rootScope.fsShow = true;
6669	};
6670	$rootScope.closeImages = function() {
6671		$rootScope.fsShow = false;
6672		$rootScope.fsList = [];
6673		$rootScope.fsIndex = 0;
6674	};
6675
6676	$angular.rootScope.debounceOptions = {
6677		updateOn: 'default blur',
6678		debounce: {
6679			blur: 0,
6680			'*': 1000
6681		}
6682	};
6683
6684	// keep last wiki search
6685	$angular.rootScope.wikiPages = [];
6686
6687	// youtube config
6688	$angular.rootScope.youtubeVars = {
6689		autoplay: 0,
6690		playsinline: 0,
6691		rel: 0,
6692		showinfo: 0,
6693		enablejsapi: 1
6694	};
6695
6696	$angular.rootScope.cronGenConfig = {
6697		use24HourTime: true,
6698		hideAdvancedTab: false
6699	};
6700
6701	// text editor toolbar
6702	$angular.rootScope.taToolbarHidden = [];
6703	$angular.rootScope.taToolbar = [
6704		['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'quote'],
6705		['fontColor', 'backgroundColor', 'bold', 'italics', 'underline', 'strikeThrough', 'ul', 'ol', 'redo', 'undo', 'clear'],
6706		['justifyLeft', 'justifyCenter', 'justifyRight', 'indent', 'outdent'],
6707		['html', 'insertImage', 'geoInsertImage', 'insertLink', 'insertVideo', 'wordcount', 'charcount'],
6708		['wikipedia']
6709	];
6710	$angular.rootScope.tinymceOptions = {
6711		min_height: 0,
6712		max_height: 400,
6713		autoresize_bottom_margin: 0,
6714		setup: function(editor) {
6715			editor.on("init", function() {
6716				editor._element = $(editor.container);
6717				editor._header = editor._element.find(".tox-editor-header");
6718				editor._footer = editor._element.find(".tox-statusbar");
6719				editor._header.hide();
6720				editor._footer.hide();
6721			});
6722			editor.on('focus', function() {
6723				editor._element.addClass("focus");
6724				editor._header.slideDown('fast');
6725				editor._footer.slideDown('fast');
6726			});
6727			editor.on('blur', function() {
6728				editor._element.removeClass("focus");
6729				editor._header.slideUp('fast');
6730				editor._footer.slideUp('fast');
6731			});
6732			
6733			editor.ui.registry.addButton('wiki', {
6734//				icon: 'insert-time',
6735      			tooltip: 'Wikipedia',
6736				text: 'W',
6737				onAction: function(_) {
6738					$angular.openModal("panel-wikipedia", {
6739						size: 'xl',
6740						editor: editor
6741					});
6742				}
6743			});
6744		},
6745		//menubar:false,
6746		//statusbar: false,
6747		plugins: [
6748			'autoresize advlist autolink link image lists charmap print preview hr anchor pagebreak',
6749			'searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking',
6750			'table emoticons template paste' // help
6751		],
6752		toolbar: 'fullscreen code wiki | undo redo | bold italic underline strikethrough removeformat | '
6753			+ 'alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | forecolor backcolor | '
6754			+ 'image media link | charmap emoticons | fontselect fontsizeselect formatselect',
6755		quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable',
6756		automatic_uploads: true,
6757		image_caption: true,
6758		relative_urls: false,
6759		remove_script_host: false,
6760		convert_urls: false,
6761		images_reuse_filename: true,
6762		valid_elements: '+*[*]',
6763		paste_enable_default_filters: false,
6764		paste_remove_styles_if_webkit: false,
6765		paste_webkit_styles: 'all',
6766		paste_retain_style_properties: 'all',
6767		images_upload_handler: function(blobInfo, success, failure, progress) {
6768
6769			if (!(blobInfo.blob() instanceof File)) {
6770				//console.log('image upload canceled due to it is not file');
6771				//failure('canceled due to it is not file');
6772				return;
6773			}
6774
6775			var xhr = new XMLHttpRequest();
6776			xhr.withCredentials = false;
6777
6778			var url = 'api/geo/' + $angular.contentScope.entity + '/' + $angular.contentScope.entityId + '/createImage.json';
6779
6780			xhr.open('POST', url);
6781			xhr.upload.onprogress = function(e) {
6782				progress(e.loaded / e.total * 100);
6783			};
6784			xhr.onerror = function() {
6785				failure('Image upload failed. Code: ' + xhr.status);
6786			};
6787			xhr.onload = function() {
6788				if (xhr.status != 200) {
6789					failure('Error: ' + xhr.status, { remove: true });
6790					return;
6791				}
6792				var json = JSON.parse(xhr.responseText);
6793				if (!json || typeof json.location != 'string') {
6794					failure('Invalid JSON: ' + xhr.responseText);
6795					return;
6796				}
6797				console.log('location: ' + json.location);
6798				success(json.location);
6799
6800				setTimeout(function() {
6801					delete $angular.contentScope.model.sys_updated;
6802					$angular.contentScope.refreshMedia();
6803				}, 1000);
6804			};
6805
6806			var formData = new FormData();
6807			formData.append('file', blobInfo.blob(), blobInfo.filename());
6808			xhr.send(formData);
6809		}
6810	};
6811
6812	if (typeof Dropzone != 'undefined') {
6813		// Dropzone - http://www.dropzonejs.com
6814		Dropzone.autoDiscover = false;
6815		$angular.rootScope.dzOptions = {
6816			paramName: 'file',
6817			parallelUploads: 1,
6818			//acceptedFiles : 'image/*',
6819			//addRemoveLinks : true,
6820			timeout: 900000, // ms
6821			maxFilesize: '500' // MB
6822		};
6823		$angular.rootScope.getDzOptions = function() {
6824			$angular.rootScope.dzOptions.dictDefaultMessage = $angular.get("$translate").instant("msg.dropzone.drop");
6825			return $angular.rootScope.dzOptions;
6826		}
6827		$angular.rootScope.dzMethods = {};
6828		$angular.rootScope.dzCallbacks = {
6829			'sending': function(file, xhr, formData) {
6830				if ($angular.contentScope.dropzoneSending) {
6831					$angular.contentScope.dropzoneSending(file, xhr, formData);
6832				}
6833			},
6834			'addedfile': function(file) {
6835				if ($angular.contentScope.dropzoneAddedFile) {
6836					$angular.contentScope.dropzoneAddedFile(file);
6837				}
6838			},
6839			'removedfile': function(file) {
6840			},
6841			'success': function(file, data) {
6842				$angular.rootScope.dzMethods.getDropzone().removeFile(file);
6843
6844				if ($angular.contentScope.dropzoneSuccessFile) {
6845					$angular.contentScope.dropzoneSuccessFile(file, data);
6846				}
6847				$utils.floatingMsg($angular.get("$translate").instant("msg.updated"));
6848			},
6849			'canceled': function(file, response) {
6850				$angular.rootScope.dzMethods.getDropzone().removeFile(file);
6851
6852				if ($angular.contentScope.dropzoneErrorFile) {
6853					$angular.contentScope.dropzoneErrorFile(file, response);
6854				}
6855
6856				var msg = "";
6857				if (response && response.message) {
6858					msg = response.message;
6859				} else {
6860					msg = $angular.get("$translate").instant("msg.error");
6861				}
6862				$utils.floatingError(msg);
6863			},
6864			'error': function(file, response) {
6865				console.log('error')
6866				$angular.rootScope.dzMethods.getDropzone().removeFile(file);
6867
6868				if ($angular.contentScope.dropzoneErrorFile) {
6869					$angular.contentScope.dropzoneErrorFile(file, response);
6870				}
6871
6872				var msg = "";
6873				if (response && response.message) {
6874					msg = response.message;
6875				} else {
6876					msg = $angular.get("$translate").instant("msg.error");
6877				}
6878				$utils.floatingError(msg);
6879			},
6880			'queuecomplete': function() {
6881				if ($angular.contentScope.dropzoneCallback) {
6882					$angular.contentScope.dropzoneCallback();
6883				}
6884			}
6885		};
6886	}
6887
6888	// columns methods
6889	$rootScope.selectComboItem = function(entity, model, columnName, $select, isCategories) {
6890		var item = $select.selected;
6891		if (item != null) {
6892			if ($utils.isArray(item) && item.length > 0) {
6893				item = item[0];
6894			}
6895			if (item.name == "..." && item.id == null) {
6896				// load more data
6897				if (isCategories) {
6898					$constants.getCategoryList(entity, $select.search, true);
6899				} else {
6900					$constants.getEntityList(entity, $select.search, true);
6901				}
6902				model[columnName] = null;
6903				return;
6904			}
6905			if (item._id) {
6906				item = item._id;
6907			}
6908			if (item.id) {
6909				item = item.id;
6910			}
6911		}
6912		model[columnName] = item;
6913		$select.close();
6914	};
6915	$rootScope.addComboItem = function(entity, model, columnName, $select, isCategories) {
6916		var item = $select ? $select.selected : null;
6917		if (item == null || model == null) {
6918			return;
6919		}
6920		if ($utils.isArray(item) && item.length > 0) {
6921			item = item[0];
6922		}
6923		if (item.name == "..." && item.id == null) {
6924			// load more data
6925			if (isCategories) {
6926				$constants.getCategoryList(entity, $select.search, true);
6927			} else {
6928				$constants.getEntityList(entity, $select.search, true);
6929			}
6930			return;
6931		}
6932		if (model[columnName] == null) {
6933			model[columnName] = [];
6934		}
6935		if (item._id) {
6936			item = item._id;
6937		}
6938		if (item.id) {
6939			item = item.id;
6940		}
6941		if (model[columnName].indexOf(item) != -1) {
6942			$utils.floatingError($angular.get("$translate").instant("msg.exists"));
6943			return;
6944		}
6945		if (!$utils.isArray(model[columnName])) {
6946			$angular.log('addComboItem: trying to add value to ' + (typeof model[columnName]))
6947		} else {
6948			model[columnName].push(item);
6949		}
6950			
6951		try {
6952			$select.close();
6953		} catch (e) {
6954		}
6955	};
6956	$rootScope.addComboAnyEntity = function(model, columnName, entity, entityId) {
6957		if (model[columnName] == null) {
6958			model[columnName] = [];
6959		}
6960		if (!$utils.isArray(model[columnName])) {
6961			return;
6962		}
6963		var item = { entity: entity, entity_id: entityId };
6964		model[columnName].push(item);
6965	};
6966	$rootScope.addToCombo = function(model, columnName, $select) {
6967		var item = $select ? $select.selected : null;
6968		if (item == null || model == null) {
6969			return;
6970		}
6971		if (model[columnName] == null) {
6972			model[columnName] = [];
6973		}
6974		if (!$utils.isArray(model[columnName])) {
6975			return;
6976		}
6977		model[columnName].push(item);
6978	};
6979	$rootScope.parseComboItem = function(item) {
6980		if (item._id) {
6981			item = item._id;
6982		}
6983		if (item.id) {
6984			item = item.id;
6985		}
6986		return item;
6987	};
6988	$rootScope.removeComboItem = function(e, model, columnName, item) {
6989		$utils.preventDefault(e);
6990		var orig = item;
6991		if (item._id) {
6992			item = item._id;
6993		}
6994		if (item.id) {
6995			item = item.id;
6996		}
6997		var list = model[columnName];
6998		if (list) {
6999			if (!$utils.isArray(list)) {
7000				list = [];
7001				model[columnName] = list;
7002			}
7003			for (var key in list) {
7004				var itemList = list[key];
7005				if (itemList == item || itemList == orig) {
7006					list.splice(key, 1);
7007					break;
7008				}
7009			}
7010		}
7011	};
7012	$rootScope.cloneComboItem = function(e, model, column, item, opts) {
7013		$utils.preventDefault(e);
7014		if (model[column.name] == null) {
7015			model[column.name] = [];
7016		}
7017		if (!$utils.isArray(model[column.name])) {
7018			$angular.log('cloneComboItem: trying to add value to ' + (typeof model[column.name]))
7019			return;
7020		}
7021		
7022		$rest.post({
7023			loading: "generic",
7024			url: contextPath + "/api/geo-new/" + column.param + "/" + item + "/copy.json",
7025			data: { copyToEntity: column.param }
7026		}).then(function(o) {
7027			model[column.name].push(o.id);
7028			$utils.autoSave(model, column, opts.extra);
7029		});
7030	};
7031	$rootScope.getColumnList = function(model, column) {
7032		if (model && column && column.column) {
7033			var l = model[column.column];
7034			if (l && $utils.isArray(l)) {
7035				return l;
7036			}
7037		}
7038		return [];
7039	};
7040	$rootScope.initTags = function(model, column) {
7041		var value = model[column];
7042		if ($utils.isArray(value)) {
7043			return value;
7044		}
7045		if (value && typeof value == 'string') {
7046			model[column] = value.split(",");
7047			return model[column];
7048		}
7049		model[column] = [];
7050		return model[column];
7051	};
7052	$rootScope.getEntityFromFormula = function(formula) {
7053		var split = formula.split("(")[1].split(",");
7054		if (formula.contains('custom_relation') && split.length > 1) {
7055			return split[1];
7056		}
7057		var result = split[0];
7058		//console.log(formula + " -> " + result)
7059		return result;
7060	};
7061	$rootScope.removeFormulaRelation = function(e, formula, list, item) {
7062		$utils.preventDefault(e);
7063
7064		var path = contextPath + "/api/geo/" + item.entity + "/" + item.id + ".json?force=true";
7065		$rest.remove({
7066			url: path,
7067			loading: "generic",
7068			confirmTitle: $angular.get("$translate").instant("msg.remove"),
7069			successMessage: $angular.get("$translate").instant('msg.removed')
7070		}).then(function(o) {
7071			$angular.contentScope.refresh();
7072		});
7073	};
7074
7075	$rootScope.validateTranslate = function(model, name) {
7076		var value = model[name];
7077		if ($utils.isObject(value)) {
7078			if (value[$locale.language]) {
7079				value = value[$locale.language];
7080			} else {
7081				var keys = $utils.keys(value);
7082				if (keys.length > 0) {
7083					value = value[keys[0]];
7084				}
7085			}
7086			model[name] = value;
7087		}
7088		return true;
7089	};
7090	$rootScope.parseTranslate = function(o) {
7091		if (typeof o == 'string') {
7092			var val = o;
7093			o = {};
7094			o[$instance.languages[0]] = val;
7095		}
7096		return o;
7097	};
7098	$rootScope.parseLangs = function(o, name) {
7099		o[name] = $rootScope.parseTranslate(o[name]);
7100	};    	
7101	$rootScope.getColumnLang = function(model) {
7102		var o = {
7103			lang : $instance.languages[0],
7104			languages : $utils.cloneList($instance.languages)
7105		};
7106		if (model && typeof model == 'object') {
7107			var hasData = !$utils.isBlank(model[o.lang]);
7108			for (var k in model) {
7109				if (!o.languages.includes(k)) {
7110					o.languages.push(k);
7111				}
7112				if (!hasData && !$utils.isBlank(model[k])) {
7113					o.lang = k;
7114					hasData = true;
7115				}
7116			}
7117		}
7118		return o;
7119	};
7120	$rootScope.gtranslate = function(model, key, lang, html) {
7121		if (model && model[key]) {
7122			var text = model[key][lang];
7123			if ($utils.isBlank(text)) {
7124				return;
7125			}
7126			//console.log(text)
7127			$angular.openModal("panel-translate", {
7128				size: 'xl',
7129				lang:lang,
7130				text:text, 
7131				html:html,
7132				successFunction: function(o) {
7133					model[key][o.lang] = o.text;
7134				}
7135			});
7136		}
7137	};
7138
7139	$rootScope.openAddEntityModal = function(entity, model, column, isList) {
7140		$angular.openModalLocal("panel-add-entity", {
7141			entity: entity,
7142			size: 'xl'
7143		}).then(function(createdObject) {
7144			if (model) {
7145				if (isList) {
7146					if (model[column] == null || !$utils.isArray(model[column])) {
7147						model[column] = [];
7148					}
7149					if (!model[column].includes(createdObject.id)) {
7150						model[column].push(createdObject.id);
7151					}
7152				} else {
7153					model[column] = createdObject.id;
7154				}
7155			}
7156			if ($angular.contentScope.submitGeoEdit) {
7157				$angular.contentScope.submitGeoEdit();
7158			}
7159		});
7160	};
7161	
7162	$rootScope.columnButtonHref = function(column, model) {
7163		if (column && model) {
7164			if (column.type === 'link' || (column.type === 'button' && column.action === 'link')) {
7165				var link = column.link;
7166				if ($utils.isBlank(link)) {
7167					// check value
7168					link = model[column.name];
7169					if ($utils.isBlank(link)) {
7170						return "";
7171					}
7172				}
7173				if (link.startsWith('/')) {
7174					link = link.substring(1);
7175				}
7176				link = $entity.parseQuery(link, model);
7177
7178				if (!link.startsWith("http") && !$constants.isCustomDomain) {
7179					if (!link.startsWith("home") && !link.startsWith("admin")) {
7180						link = $instance.getBaseUrl() + link;
7181					}
7182				}
7183				return link;
7184			}
7185		}
7186		return "";
7187	};
7188
7189	$rootScope.isColumnLinkBlank = function(column, model) {
7190		var link = $rootScope.columnButtonHref(column, model);
7191		return link && link.startsWith('http');
7192	};
7193	
7194	$angular.searchRootModel = function() {
7195		if ($("div.panel-add-entity.show").length > 0) {			// panel-add-entity
7196			return $angular.panelAddEntityScope.model;
7197		}		
7198		if ($angular.contentScope.webFormItem) {					// web-home	
7199			var item = $angular.contentScope.webFormItem;
7200			if (item.type == 'formview') {
7201				return item.form.data;
7202			}
7203			return item.model;
7204		}
7205		if ($angular.contentScope.form) {							// web-item 
7206			return $angular.contentScope.form.data;
7207		}		
7208		if ($angular.contentScope.geoForm) {						// geo-new, geo-edit
7209			return $angular.contentScope.geoForm.data;
7210		}		
7211		if ($angular.contentScope.model) {							// others 
7212			return $angular.contentScope.model;
7213		}		
7214		$angular.log('Root model not found');
7215		return null;
7216	};
7217	
7218	$angular.searchFormView = function() {
7219		if ($("div.panel-add-entity.show").length > 0) {			// panel-add-entity
7220			return $angular.panelAddEntityScope.geoForm;
7221		}		
7222		if ($angular.contentScope.webFormItem) {					// web-home	
7223			var item = $angular.contentScope.webFormItem;
7224			if (item.type == 'formview') {
7225				return item.form;
7226			}
7227			return null;
7228		}
7229		if ($angular.contentScope.form) {							// web-item 
7230			return $angular.contentScope.form;
7231		}		
7232		if ($angular.contentScope.geoForm) {						// geo-new, geo-edit 
7233			return $angular.contentScope.geoForm;
7234		}		
7235		return null;
7236	};
7237	
7238	$rootScope.pushButton = function(column, model, saved, actions, index) {
7239	
7240		if (saved == null && actions == null) {
7241			console.log('pushButton start');
7242		}
7243		
7244		// entity required
7245		var root = $angular.searchRootModel();
7246		var entity = model.entity;
7247		if (entity == null && root) {
7248			entity = root.entity;
7249		}
7250		if (model.id == null && root.id) {
7251			model.id = root.id; // set id
7252		}
7253		if (entity == null) {
7254			$angular.buttonError(root, column, model, 'Button: entity not found');
7255			return;
7256		}
7257		
7258		var callbackNext = function() {
7259			setTimeout(function() {
7260				// show only if no floating showing
7261				console.log('pushButton done');
7262				if ($("div.floating-msg").length > 0) {
7263					return;
7264				}
7265				$utils.floatingMsg($angular.get("$translate").instant('msg.done'));
7266			}, 500);
7267		};
7268		if (actions != null) {
7269			if (index == actions.length) {
7270				callbackNext();
7271				return; // end multi actions
7272			}
7273			var path = actions[index];
7274			column = $entity.getColumn(entity, path);
7275			if (column == null) {
7276				$angular.buttonError(column, model, 'Button: action not found: ' + path);
7277				return;
7278			}
7279			callbackNext = function() {
7280				$rootScope.pushButton(column, model, saved, actions, index+1);
7281			}
7282		}
7283		if (column == null || model == null) {
7284			$angular.buttonError(root, column, model, "Button: column/model not found");
7285			return;
7286		}
7287		var path = column.name;
7288		if (column.path != null) {
7289			path = column.path;
7290		}
7291		if ($utils.isBlank(path)) {
7292			$angular.buttonError(root, column, model, "Button: path not found");
7293			return;
7294		}
7295		var action = column.action;
7296		if ($utils.isBlank(action)) {
7297			$angular.buttonError(root, column, model, "Action required");
7298			return;
7299		}
7300		
7301		if (action === 'save' || (column.pre_save && saved == null)) {
7302		
7303			console.log('pushButton action: ' + action + " (saving)");
7304			
7305			// prepare callback after save
7306			var callback = null;
7307			if (action != 'save' || (actions != null && index < actions.length)) {
7308				// has pending actions after save
7309				callback = function(callbackPostSave) {
7310					console.log('pushButton action: ' + action + " (postSave)");
7311					if (action != 'save') {
7312						// has the column action pending
7313						$rootScope.pushButton(column, model, true, actions, index);
7314						return;
7315					}
7316					if (actions != null && index < actions.length) {
7317						// has pending actions
7318						callbackNext();
7319						return;
7320					}
7321					if (callbackPostSave) {
7322						callbackPostSave();
7323						return;
7324					}	
7325					callbackNext();
7326				}
7327			}
7328			
7329			if ($("div.panel-add-entity.show").length > 0) {			// panel-add-entity
7330				$angular.panelAddEntityScope.save(callback);
7331				
7332			} else if ($angular.contentScope.saveForm) {
7333				$angular.contentScope.saveForm(callback);				// web-home, web-item
7334			
7335			} else if ($angular.contentScope.submitGeoEdit) {
7336				$angular.contentScope.submitGeoEdit(false, callback);	// geo-edit 
7337			
7338			} else if ($angular.contentScope.createRaw) {
7339				$angular.contentScope.createRaw(false, callback);		// geo-new
7340			
7341			} else {
7342				$angular.buttonError(root, column, model, 'No save function found')
7343			}
7344			return;
7345		}
7346		
7347		console.log('pushButton action: ' + action);
7348		
7349		if (action === 'actions') {
7350			var actions = column.options;
7351			if (actions == null) {
7352				$angular.buttonError(root, column, model, 'No actions found');
7353				return;
7354			}
7355			// start action index 0
7356			$rootScope.pushButton(column, model, saved, actions, 0);
7357			return;
7358		}
7359		
7360		if (column.action === 'link') {
7361			var link = $rootScope.columnButtonHref(column, model);
7362			if (!$utils.isBlank(link)) {
7363				
7364				var openNew = link.startsWith("http");
7365				if (!openNew) {
7366					if (link.contains("admin/") && $constants.isWeb) {
7367						openNew = true;
7368					} else if (link.contains("home/") && !$constants.isWeb) {
7369						openNew = true;
7370					}
7371				}
7372				console.log("pushButton: going to " + link);
7373				if (openNew) {
7374					const enlace = document.createElement("a");
7375					enlace.href = link;
7376					enlace.target = "_blank";
7377					enlace.rel = "noopener noreferrer"; // seguridad adicional
7378					enlace.click();
7379					//window.open(link, '_blank'); // deberia funcionar si se ejecuta dentro de onclick
7380					//window.location.href = link;
7381				} else {
7382					var $location = $angular.get("$location");
7383					
7384					var parts = link.split('?');
7385					var path = parts[0];
7386					$location.path(path);
7387					
7388					if (parts.length > 1) {
7389						var params = {};
7390						var queryString = parts[1];
7391						queryString.split('&').forEach(function(pair) {
7392						    const [key, value] = pair.split('=');
7393						    params[key] = value;
7394						});
7395						$location.search(params);
7396					}
7397				}
7398			}
7399			callbackNext();
7400			return;
7401		}
7402		
7403		if (column.action === 'back') {
7404			$events.back();
7405			callbackNext();
7406			return;
7407		}
7408		
7409		if (column.action === 'reset') {
7410			if ($angular.contentScope && $angular.contentScope.resetForm) {
7411				$angular.contentScope.resetForm();
7412			} else {
7413				$angular.buttonError(root, column, model, 'No reset function found')
7414			}
7415			callbackNext();
7416			return;
7417		}
7418		
7419		// entityId required
7420		var id = model.id;
7421		if (id == null && root) {
7422			id = root.id;
7423		}
7424		if (id == null) {
7425			$angular.buttonError(root, column, model, 'Button: entity id not found');
7426			return;
7427		}
7428		
7429		if (column.action === 'download') {
7430			if (column.spreadsheet == null) {
7431				$angular.buttonError(root, column, model, 'missing spreadsheet to download')
7432				return;
7433			}
7434
7435			var url = contextPath + "/api/geo/" + entity + "/" + id
7436				+ "/download.xlsx?geo_instance=" + $instance.getId() + "&_tz=-120&spreadsheet=" + column.spreadsheet;
7437			var alink = document.createElement('a');
7438			alink.href = url;
7439			alink.download = entity + "#" + id + ".xlsx";
7440			alink.target = "_blank";
7441			document.body.appendChild(alink);
7442			alink.click();
7443			document.body.removeChild(alink);
7444			
7445			callbackNext();
7446			return;
7447		}
7448		
7449		if (column.action === 'print') {
7450			if (column.spreadsheet == null) {
7451				$angular.buttonError(root, column, model, 'missing spreadsheet to print')
7452				return;
7453			}
7454		
7455			var fileUrl = contextPath + "/api/geo/" + entity + "/" + id
7456					+ "/download.xlsx?__keep=true&geo_instance=" + $instance.getId() + "&_tz=-120&spreadsheet=" + column.spreadsheet;
7457					
7458			var num = column.number;		
7459			if (num == null || num < 1) {
7460				num = 1;
7461			}					
7462			$utils.printUrl(fileUrl, num);
7463			callbackNext();
7464			return;
7465		}
7466
7467		if (column.action === 'move') {
7468			if (column.param == null) {
7469				$angular.buttonError(root, column, model, 'button: error action -> data required (move)');
7470				return;
7471			}
7472			$rest.post({
7473				loading: "generic",
7474				url: contextPath + "/api/geo/" + entity + "/" + id + "/move.json",
7475				data: {
7476					target: column.param
7477				},
7478				confirmTitle: column._label
7479			}).then(function(o) {
7480				o.entity = o.target;
7481				var url = $angular.rootScope.getItemLinkBy(o);
7482				console.log('going to ' + url, o)
7483				$angular.get("$location").path(url).search({}).replace();
7484				callbackNext();
7485			});
7486			return;
7487		}
7488
7489		if (column.action === 'copy' || column.action === 'copy_to_column') {
7490			if (column._label == null || column.param == null) {
7491				$angular.buttonError(root, column, model, 'button: error action -> data required (copy)');
7492				return;
7493			}
7494			var data = {
7495				copyToEntity: column.param
7496			};
7497			if (column.action === 'copy_to_column') {
7498				data.copyToColumn = column.column;
7499			}
7500			$rest.post({
7501				loading: "generic",
7502				url: contextPath + "/api/geo-new/" + entity + "/" + id + "/copy.json",
7503				data: data,
7504				confirmTitle: column._label
7505			}).then(function(o) {
7506				if (column.action === 'copy') {
7507					var url = $angular.rootScope.getItemLinkBy(o);
7508					$angular.get("$location").path(url).search({});
7509				} else {
7510					if ($angular.contentScope && $angular.contentScope.refresh) {
7511						$angular.contentScope.refresh();
7512					}
7513				}
7514				callbackNext();
7515			});
7516			return;
7517		}
7518		
7519		// server request
7520		$rest.post({
7521			loading: "generic",
7522			url: contextPath + "/api/geo/" + entity + "/" + id + "/pushButton.json",
7523			data: {
7524				column:column
7525			}
7526		}).then(function() {
7527			if ($angular.contentScope && $angular.contentScope.refresh) {
7528				$angular.contentScope.refresh(callbackNext);
7529			} else {
7530				callbackNext();
7531			}
7532		});
7533	}
7534	
7535	$angular.buttonError = function(root, column, model, msg) {
7536		$angular.log(msg);
7537		console.log('root', root);
7538		console.log('column', column);
7539		console.log('model', model);
7540		$utils.floatingError($angular.get("$translate").instant('msg.error'));
7541	};
7542
7543	$rootScope.openAdmin = function() {
7544		window.location.href = contextPath + "/admin";
7545	};
7546
7547	$rootScope.carouselNext = function(e, o) {
7548		$utils.preventDefault(e);
7549		var c = searchCarousel(o);
7550		if (c) {
7551			c.next();
7552		}
7553	};
7554	$rootScope.carouselPrev = function(e, o) {
7555		$utils.preventDefault(e);
7556		var c = searchCarousel(o);
7557		if (c) {
7558			c.prev();
7559		}
7560	};
7561	function searchCarousel(o) {
7562		if (o.carousel) {
7563			return o.carousel;
7564		}
7565		if (o.$$childHead && o.$$childHead.carousel) {
7566			return o.$$childHead.carousel;
7567		}
7568		var i=0;
7569		var o2 = o;
7570		while (o2.$parent != null) {
7571			o2 = o2.$parent;
7572			if (o2.carousel) {
7573				return o2.carousel;
7574			}
7575			i++;
7576			if (i == 5) {
7577				break;
7578			}
7579		}
7580		console.log('not found carousel', o);
7581		return null;
7582	}
7583
7584	// methods to add/remove items
7585	$rootScope.addList = function(o, field) {
7586		var list = o[field];
7587		if (list == null) {
7588			list = [];
7589			o[field] = list;
7590		}
7591		list.push("");
7592	};
7593	$rootScope.removeList = function(o, field, index) {
7594		var list = o[field];
7595		if (list != null) {
7596			list.splice(index, 1);
7597		}
7598	};
7599	$rootScope.editJson = function() {
7600		$angular.openModal('panel-edit-json', {
7601			json: $angular.contentScope.model,
7602			size: 'lg',
7603			successFunction: function(json) {
7604				$angular.contentScope.model = json;
7605			}
7606		});
7607	};
7608	$rootScope.viewJson = function() {
7609		$angular.openModal("panel-json", { json:$angular.contentScope.model, size:'xl' });
7610	};
7611
7612	return true;
7613};
7614
7615$angular.goHome = function() {
7616	if ($instance.current && $constants.isWeb) {
7617		var o = $instance.current.geo_web;
7618		if (o && o.menu && o.menu.length > 0) {
7619			$angular.apply($angular.rootScope, function() {
7620				$angular.get("$location").path($instance.getBaseUrl() + "p/" + o.menu[0].id).search({}).replace();
7621			});
7622			return;
7623		}
7624	}
7625
7626	var path;
7627	if (!$constants.isWeb) {
7628		path = "/admin";
7629	} else {
7630		path = "/home";
7631	}
7632	$angular.apply($angular.rootScope, function() {
7633		$angular.get("$location").path(path).search({}).replace();
7634	});
7635};
7636
7637$angular.goLogin = function() {
7638	if ($instance.isMslogin()) {
7639		
7640		// FIXME 00000 do it at backend
7641		var accessToken = $cookiesService.get("ms_access_token"); 
7642		if (accessToken != null) {
7643		
7644			var form = '<form>';
7645			form += '\n<input name="username" value="ms#'+accessToken+'" />';
7646			form += '\n<input name="password" value="ms" />';
7647			form += '\n<input name="_remember_me" value="true" />';
7648			form += '\n</form>';
7649			
7650			$rest.post({
7651				loading: "generic",
7652				url: contextPath + "/log-in",
7653				contentType: "urlencoded",
7654				form: form,
7655				showError: false
7656			}).then(function() {
7657				// ok
7658				window.location.href = contextPath + $angular.tmpPath;
7659
7660			}).catch(function(e) {
7661				$utils.floatingError($angular.get("$translate").instant("msg.login.error"));
7662				$cookiesService.remove("ms_access_token");
7663				
7664				$angular.goLogin();
7665				//window.location.href = contextPath + "/admin";
7666			});
7667			return;
7668		}
7669		
7670		// FIXME 00000 remove instance cookie
7671		$cookiesService.put("instance", $instance.getId(), 1);
7672		window.location.href = "ms/a/login.go";
7673		return;
7674	}
7675	var path;
7676	if (!$constants.isWeb) {
7677		path = "/admin/login";
7678	} else {
7679		path = $instance.getBaseUrl() + "login";
7680	}
7681	$angular.apply($angular.rootScope, function() {
7682		$angular.get("$location").path(path).replace();
7683	});
7684};
7685
7686$angular.changeLanguage = function(e, lang) {
7687	$events.closeAll();
7688	$utils.preventDefault(e);
7689
7690	//console.log('* ' + $locale.language + " -> " + lang)
7691	if ($locale.language == lang) {
7692		return;
7693	}
7694
7695	// apply texts and content
7696	$angular.get("$translate").use(lang);
7697	$cookiesService.put("_language", lang, 90);
7698	
7699	setTimeout(function() {
7700		$locale.language = lang; // save last user selection
7701		$constants.clean();
7702		$angular.get("$route").reload(); // force full reload
7703	}, 60);
7704
7705	// close menu if needed
7706	setTimeout(function() {
7707		if ($angular.element("#menu").hasClass("menu-open")) {
7708			$events.toggleMenu();
7709		}
7710		if ($angular.element("#cartAsMenu").hasClass("menu-open")) {
7711			$events.toggleCartAsMenu();
7712		}
7713	}, 100);
7714
7715};
7716
7717//select methods
7718$angular.addComboItem = function(e, model, columnName, item) {
7719	$utils.preventDefault(e);
7720	//console.dir(item)
7721	if (item == null || model == null) {
7722		return;
7723	}
7724	if ($utils.isArray(item) && item.length > 0) {
7725		item = item[0];
7726	}
7727	if (model[columnName] == null) {
7728		model[columnName] = [];
7729	}
7730	if (item._id) {
7731		item = item._id;
7732	}
7733	if (item.id) {
7734		item = item.id;
7735	}
7736	if (!$utils.isArray(model[columnName])) {
7737		model[columnName] = [];
7738	}
7739	if (model[columnName].indexOf(item) != -1) {
7740		$utils.floatingError($angular.get("$translate").instant("msg.exists"));
7741		return;
7742	}
7743	model[columnName].push(item);
7744	$utils.floatingWarn($angular.get("$translate").instant("msg.added"));
7745};
7746$angular.removeComboItem = function(e, model, columnName, item) {
7747	$utils.preventDefault(e);
7748	if (item._id) {
7749		item = item._id;
7750	}
7751	if (item.id) {
7752		item = item.id;
7753	}
7754	var list = model[columnName];
7755	if (list) {
7756		if (!$utils.isArray(list)) {
7757			list = [];
7758			model[columnName] = list;
7759		}
7760		for (var key in list) {
7761			var itemList = list[key];
7762			if (itemList == item) {
7763				list.splice(key, 1);
7764				break;
7765			}
7766		}
7767	}
7768};
7769
7770//back button
7771window.onpopstate = function(e) {
7772	// force load page
7773	$angular.apply($angular.rootScope);
7774};
7775
7776$angular.openModalLocal = function(name, params) {
7777	var _modal = $angular.get("$uibModal").open({
7778		templateUrl: name + ".html",
7779		controller: name,
7780		windowTopClass: name,
7781		size: params ? params.size : null,
7782		resolve: {
7783			params: function() {
7784				return params;
7785			}
7786		}
7787	});
7788	_modal.opened.then(function() {
7789	});
7790	_modal.closed.then(function() {
7791	});
7792	return _modal.result;
7793};
7794
7795$angular.openModal = function(name, params) {
7796	return $angular.openModalLocal(name, params);
7797};
7798
7799$angular.currentRoute = "";
7800$angular.resolveRoute = function(name, requiredLogged, publicUrl) {
7801	var controller = name;
7802	return {
7803		controller: controller,
7804		templateUrl: name + ".html",
7805		reloadOnSearch: false,
7806		resolve: {
7807			load: function($q, $rootScope) {
7808				return $angular.resolveRoutePage($q, $rootScope, name, controller, requiredLogged, publicUrl);
7809			}
7810		}
7811	}
7812};
7813
7814$angular.resolveRoutePage = function($q, $rootScope, name, controller, requiredLogged, publicUrl) {
7815	// remove window events
7816	$(window).unbind();
7817
7818	// close panels
7819	$events.closeAll();
7820	if ($rootScope.closeSearch) {
7821		$rootScope.closeSearch(true);
7822	}
7823
7824	if (!$utils.isBlank($rest.version) && $rest.version !== appVersion) {
7825		// version changed -> reload
7826		console.log('reloading ' + appVersion + " -> " + $rest.version);
7827		window.location.reload();
7828		// continue execution to avoid errors
7829	}
7830
7831	if (typeof gtag != 'undefined') {
7832		var url = $angular.get("$location").path();
7833		if (gaInstance != "") {
7834			gtag('event', 'page_view', {
7835				page_location: url,
7836				send_to: gaInstance
7837			});
7838		}
7839	}
7840
7841	var defer = $q.defer();
7842
7843	$rootScope.skipHideLoading = true; // for resolve instance
7844
7845	//	if (controller == "home-instance") {
7846	//		// reload instance data
7847	//		//console.dir($instance.current)
7848	//		$instance.current = null;
7849	//	}
7850	var toExecute = function() {
7851
7852		if ($instance.isLoginRequired(requiredLogged, publicUrl)) {
7853			var path = $angular.get("$location").path();
7854			var isLoginPage = path.contains('/login');
7855			if (!isLoginPage) {
7856				// save path to resend after login
7857				$angular.tmpPath = path;
7858				$angular.goLogin();
7859				return;
7860			}
7861		}
7862
7863		// set the current page class in body
7864		$angular.currentRoute = controller;
7865
7866		// set instance properties
7867		$instance.setCssProperties();
7868
7869		defer.resolve();
7870
7871		$angular.apply($rootScope, function() {
7872			$angular.pageReady();
7873		});
7874	};
7875
7876	if (!$user.isInit) {
7877		$user.isInit = true;
7878		var callback = toExecute;
7879		toExecute = function() {
7880			$user.set(callback);
7881		};
7882	}
7883
7884	$instance.resolveInstance(toExecute);
7885
7886	return defer.promise;
7887
7888};
7889
7890$angular.pageReady = function() {
7891	setTimeout(function() {
7892		$angular.pageReady2();		
7893	}, 250);
7894};
7895$angular.pageReady2 = function() {
7896	if (!$("#header").hasClass('hidden')) {
7897		return;
7898	}
7899	
7900	if (!$angular.rootScope.skipHideLoading) {
7901		$utils.hideLoading();
7902	}
7903	$("#init-loading").remove();
7904	$("#oldbrowser").remove();
7905	$("#header").removeClass("hidden");
7906	$("#search").removeClass("hidden");
7907	$("#searchList").removeClass("hidden");
7908	$("#menu").removeClass("hidden");
7909	$("#cartAsMenu").removeClass("hidden");
7910	$("#content").removeClass("hidden");
7911	$("#footer").removeClass("hidden");
7912	$init.log("app ready");
7913}
7914
7915$angular.preparePage = function(params) {
7916
7917	if (params == null) {
7918		params = {};
7919	}
7920
7921	if ($angular.menuScope) {
7922		$angular.menuScope.selectedPage = "";
7923	}
7924
7925	$events.refresh(params.resetBacks);
7926
7927	if (params.title && params.title != null) {
7928		$angular.setTitle(params.title);
7929	} else {
7930		$angular.setTitle("");
7931	}
7932
7933	if ($angular.headerScope) {
7934		$angular.headerScope.bar_buttons_right = undefined;
7935		if (params.buttonsRight && params.buttonsRight != null) {
7936			$angular.headerScope.bar_buttons_right = params.buttonsRight;
7937		}
7938		$angular.headerScope.bar_buttons_right_right = undefined;
7939		if (params.buttonsRightRight && params.buttonsRightRight != null) {
7940			$angular.headerScope.bar_buttons_right_right = params.buttonsRightRight;
7941		}
7942	}
7943};
7944
7945$angular.setTitle = function(title) {
7946	$angular.title = title;
7947};
7948
7949$angular.mapDark = [
7950	{ elementType: 'geometry', stylers: [{ color: '#242f3e' }] },
7951	{ elementType: 'labels.text.stroke', stylers: [{ color: '#242f3e' }] },
7952	{ elementType: 'labels.text.fill', stylers: [{ color: '#746855' }] },
7953	{
7954		featureType: 'administrative.locality',
7955		elementType: 'labels.text.fill',
7956		stylers: [{ color: '#d59563' }]
7957	},
7958	{
7959		featureType: 'poi',
7960		elementType: 'labels.text.fill',
7961		stylers: [{ color: '#d59563' }]
7962	},
7963	{
7964		featureType: 'poi.park',
7965		elementType: 'geometry',
7966		stylers: [{ color: '#263c3f' }]
7967	},
7968	{
7969		featureType: 'poi.park',
7970		elementType: 'labels.text.fill',
7971		stylers: [{ color: '#6b9a76' }]
7972	},
7973	{
7974		featureType: 'road',
7975		elementType: 'geometry',
7976		stylers: [{ color: '#38414e' }]
7977	},
7978	{
7979		featureType: 'road',
7980		elementType: 'geometry.stroke',
7981		stylers: [{ color: '#212a37' }]
7982	},
7983	{
7984		featureType: 'road',
7985		elementType: 'labels.text.fill',
7986		stylers: [{ color: '#9ca5b3' }]
7987	},
7988	{
7989		featureType: 'road.highway',
7990		elementType: 'geometry',
7991		stylers: [{ color: '#746855' }]
7992	},
7993	{
7994		featureType: 'road.highway',
7995		elementType: 'geometry.stroke',
7996		stylers: [{ color: '#1f2835' }]
7997	},
7998	{
7999		featureType: 'road.highway',
8000		elementType: 'labels.text.fill',
8001		stylers: [{ color: '#f3d19c' }]
8002	},
8003	{
8004		featureType: 'transit',
8005		elementType: 'geometry',
8006		stylers: [{ color: '#2f3948' }]
8007	},
8008	{
8009		featureType: 'transit.station',
8010		elementType: 'labels.text.fill',
8011		stylers: [{ color: '#d59563' }]
8012	},
8013	{
8014		featureType: 'water',
8015		elementType: 'geometry',
8016		stylers: [{ color: '#17263c' }]
8017	},
8018	{
8019		featureType: 'water',
8020		elementType: 'labels.text.fill',
8021		stylers: [{ color: '#515c6d' }]
8022	},
8023	{
8024		featureType: 'water',
8025		elementType: 'labels.text.stroke',
8026		stylers: [{ color: '#17263c' }]
8027	}
8028];
8029
8030$angular.initComponent = function($scope, initFunction, config) {
8031	if ($scope && $scope.$broadcast) {
8032		$scope.$broadcast(initFunction, config);
8033	} else if ($angular.contentScope && $angular.contentScope.$broadcast) {
8034		$angular.contentScope.$broadcast(initFunction, config);
8035	} else if ($angular.rootScope && $angular.rootScope.$broadcast) {
8036		$angular.rootScope.$broadcast(initFunction, config);
8037	} else {
8038		throw new Error('$broadcast is not defined when execute init function component: ' + initFunction);
8039	}
8040};
8041
8042$angular.acceptLicense = function(id, email, callback) {
8043	if (id == null) {
8044		id = "a";
8045	}
8046	$rest.put({
8047		url: contextPath + "/www/license/"+id+"/accept.json",
8048		data: { email:email },
8049		loading:"generic"
8050	}).then(function(o) {
8051		callback(); 
8052	});
8053};
8054
8055$angular.rejectLicense = function(id, email, callback) {
8056	if (id == null) {
8057		id = "a";
8058	}
8059	$rest.put({
8060		url: contextPath + "/www/license/"+id+"/reject.json",
8061		data: { email:email },
8062		loading:"generic"
8063	}).then(function(o) {
8064		callback(); 
8065	});
8066};
8067
8068$angular.initGA = function() {
8069	if ($constants.isDev) {
8070		return;
8071	}
8072	
8073	// Google Tag (gtag.js) - Modern GA4 & Google Ads
8074	if (gaInstance != "") {
8075		window.dataLayer = window.dataLayer || [];
8076		window.gtag = function() { window.dataLayer.push(arguments); };
8077		window.gtag('js', new Date());
8078		window.gtag('config', gaInstance);
8079
8080		var script = document.createElement('script');
8081		script.type = 'text/javascript';
8082		script.async = true;
8083		script.src = 'https://www.googletagmanager.com/gtag/js?id=' + gaInstance;
8084		document.head.appendChild(script);
8085	}
8086};
8087
8088// service worker
8089$angular.initServiceWorker = function() {
8090	
8091	if ('serviceWorker' in navigator) {
8092		
8093		var isActive = false;//!isPro;
8094		
8095		// NO use service worker
8096		navigator.serviceWorker.getRegistrations().then(function(registrations) {
8097			registrations.forEach(function(registration) {
8098				if (isActive && registration.scope != null && !registration.scope.includes('/admin')) {
8099					// keep
8100				} else {
8101					// unregister					
8102	  				registration.unregister();
8103					console.log('End sw ' + registration.scope);
8104				}
8105			})
8106		})
8107		
8108		if (isActive) {
8109			// use service worker
8110			var url = contextPath + '/static/sw.js';
8111			var scope = './';//$constants.isWeb ? './' : './admin';
8112			navigator.serviceWorker.register(url, {scope:scope}).then(function(registration) {
8113				if (registration.waiting) {
8114					registration.waiting.postMessage('SKIP_WAITING');
8115				}
8116				registration.addEventListener('updatefound', function() {
8117					if (registration.installing) {
8118						registration.installing.addEventListener('statechange', function() {
8119							if (registration.waiting) {
8120								if (navigator.serviceWorker.controller) {
8121									registration.waiting.postMessage('SKIP_WAITING');
8122								} else {
8123									console.log('Init sw ' + scope);
8124								}
8125							}
8126						})
8127					}
8128				})
8129			}, function(error) {
8130				$angular.log('Service worker registration failed', error);
8131			});
8132		}
8133	}
8134}
8135
8136$angular.mapReady = false;
8137$angular.initMap = function(callback) {
8138	
8139	window.gak = window.gak || "";
8140	if (!window.gak) {
8141        console.warn("Google Maps API key no disponible. Saltando carga.");
8142        if (typeof callback === "function") callback(null);
8143        return;
8144    }
8145	
8146	(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
8147    	key: window.gak, v: "weekly", libraries: "drawing,places,geometry" //marker
8148  	});
8149	initMap(callback);
8150}
8151async function initMap(callback) {
8152	const { Map } = await google.maps.importLibrary("maps");
8153	//const { AdvancedMarkerElement, PinElement } = await google.maps.importLibrary("marker");
8154	$angular.mapReady = true;
8155	if (callback != null) {
8156		callback();
8157	}
8158	//console.log("map ready")
8159}
8160
8161//// File utils.js ////
8162var $utils = {};
8163
8164$utils.postSave = function(old, data, isCreate, callbackPushButton, callbackEnd) {
8165	
8166//	if (old) {
8167//		if (old.id == null && data.id) {
8168//			old.id = data.id; // set id
8169//		}
8170//		if (old.sys_updated) {
8171//			delete old.sys_updated; // remove sysupdated because now is not fresh
8172//		} 
8173//	}
8174	
8175	// remove sysupdated
8176	if ($angular.contentScope && $angular.contentScope.model) {
8177		delete $angular.contentScope.model.sys_updated; // because now is not fresh
8178	}
8179	
8180	var callback1 = function() {
8181		var $translate = $angular.get("$translate");
8182		var message = $utils.getGeoTranslation(isCreate ? 'success_message_on_create' : 'success_message_on_update');
8183		if (message) {
8184			// panel with msg
8185			$utils.hideLoading();
8186			$angular.openModal("panel-message", {
8187				text: $translate.instant(message, data)
8188			}).then(function() {
8189			});
8190			return;
8191		}
8192		
8193		// floating
8194		$utils.floatingMsg($translate.instant(isCreate ? 'msg.created' :  'msg.updated'));
8195	};
8196	
8197	var callback2 = function() {
8198		if (callbackEnd) {
8199			callbackEnd(callback1);
8200			return;
8201		}
8202		callback1();
8203	};
8204
8205	if (callbackPushButton) {
8206		callbackPushButton(callback2);
8207		return;
8208	}
8209	callback2();
8210	
8211};
8212
8213$utils.validateForm = function(form, errorMessage) {
8214	if (form && !form.$valid && form.$error) {
8215		var msg = errorMessage;
8216		if (!errorMessage) {
8217			for (var k in form.$error) {
8218				var errors = form.$error[k];
8219				for (var k2 in errors) {
8220				
8221					var cause;
8222					if (k == 'required') {
8223						cause = $angular.get("$translate").instant("msg.required");
8224					} else {
8225						cause = $angular.get("$translate").instant("msg.invalid");
8226					}				
8227					
8228					var field = errors[k2];
8229					var label;
8230					if (field.$name) {
8231						label = $utils.parseGeoTranslation(field.$name);
8232					} else {
8233						label = field.$$element.parents('.form-group').eq(0).find('.control-label').clone().children().remove().end().text();
8234					}
8235//					var validationValue = field.$$attr[k];
8236//					if ($utils.isTrue(validationValue)) {
8237//						validationValue = '';
8238//					}
8239					var msg = label.trim() + " " + cause.toLowerCase();
8240					$utils.floatingError(msg);
8241					console.log('form validation', k, field);
8242					return false;
8243				}
8244			}
8245		}
8246	}
8247	return true;
8248};
8249
8250$utils.serializeFormJSON = function(form) {
8251	var o = {};
8252	var a = form.serializeArray();
8253	$.each(a, function() {
8254		if (o[this.name]) {
8255			if (!o[this.name].push) {
8256				o[this.name] = [o[this.name]];
8257			}
8258			o[this.name].push(this.value || '');
8259		} else {
8260			o[this.name] = this.value || '';
8261		}
8262	});
8263
8264	// set boolean to false
8265	$('input:checkbox[value="true"]').each(function() {
8266		if (this.name != "" && o[this.name] == null && !this.checked) {
8267			o[this.name] = "false";
8268		}
8269	});
8270
8271	return o;
8272};
8273
8274$utils.get = function(data, path) {
8275	if (path) {
8276		var split = path.split(".");
8277		for (var k in split) {
8278			if (data == null) {
8279				return null;
8280			}
8281			var key = split[k];
8282			data = data[key];
8283		}
8284	}
8285	return data;
8286};
8287
8288$utils.preventDefault = function(e) {
8289	if (e) {
8290		e.preventDefault();
8291		e.stopPropagation();
8292	}
8293};
8294
8295$utils.isScreenLarge = window.innerWidth >= 992;
8296$utils.isScreenSmall = window.innerWidth <= 768;
8297
8298$utils.isString = function(o) {
8299	return o == null || typeof o == 'string';
8300};
8301$utils.isObject = function(o) {
8302	return o != null && typeof o == 'object';
8303};
8304$utils.isArray = function(obj) {
8305	return obj && angular.isArray(obj);
8306};
8307
8308$utils.isLink = function(o) {
8309	if (o && typeof o == 'string') {
8310		return o.startsWith('http');
8311	}
8312	return false;
8313};
8314
8315$utils.getColor = function(o, preffix) {
8316	if (!$utils.isBlank(o)) {
8317		if (o.startsWith('#')) {
8318			o = o.substring(1);
8319		}
8320		if (!$utils.isBlank(preffix)) {
8321			o = preffix + o;
8322		}
8323		o = '#' + o;
8324	}
8325	return o;
8326};
8327$utils.getColorOpaque = function(c) {
8328	if (c && c.startsWith('#')) {
8329		return c + "33";
8330	}
8331	return c;
8332};
8333
8334$utils.cleanHtml = function(html) {
8335	var temporalDivElement = document.createElement("div");
8336	temporalDivElement.innerHTML = html;
8337	return temporalDivElement.textContent || temporalDivElement.innerText || "";
8338};
8339
8340$utils.parseToInstagram = function(value) {
8341	// https://www.instagram.com/p/"+item+"/embed
8342	if (value && value.contains('instagram.com/p')) {
8343		// add embed if needed
8344		if (!value.contains('/embed')) {
8345			if (!value.endsWith('/')) {
8346				value += "/";
8347			}
8348			value += "embed";
8349		}
8350	}
8351	return value;
8352};
8353
8354$utils.parseHtml = function(o) {
8355	var val = $utils.parse(o);
8356	return $angular.get('$sce').trustAsHtml(val);
8357}
8358
8359$utils.parse = function(o) {
8360	if (o == null) {
8361		return "";
8362	}
8363	
8364	if (typeof o == 'string') {
8365		return o;
8366	}
8367	
8368	if (typeof o == 'number') {
8369//		if(Math.round(o) !== o) { -> number of decimals configured in formula
8370//            o = o.toFixed(2);
8371//        }
8372		return $utils.parseNumber(o);	
8373	}
8374	
8375	if (o === true) { 
8376		return "\u2714";
8377	}
8378	if (o === false) {
8379		//return "\u2717";
8380		return "false";
8381	}
8382	
8383	if ($utils.isArray(o)) {
8384		if (o.length == 0) {
8385			return "";
8386		}
8387		if (o.length === 1) {
8388			return $utils.parse(o[0]);
8389		}
8390		if (typeof o[0] == 'object') {
8391			var list = [];
8392			for (var key in o) {
8393				list.push($utils.parse(o[key]));
8394			}
8395			o = list;
8396		}
8397		return $utils.join(o, ", ");
8398	}
8399	
8400	if (typeof o != 'object') {
8401		$angular.log('Unknown type ' + (typeof o) + " with value " + o);
8402		return "" + o;
8403	}
8404	
8405	if (o.entity && o.id) {
8406		return $entity.displayColumn(o);
8407	}
8408
8409	//console.log($utils.keys(o));
8410	var val = o[$locale.language];
8411	if (!$utils.isBlank(val)) {
8412		return val;
8413	}
8414	for (var key in $instance.languages) {
8415		var lang = $instance.languages[key];
8416		if (!$utils.isBlank(o[lang])) {
8417			return o[lang];
8418		}
8419	}
8420	for (var key in o) {
8421		if (!$utils.isBlank(o[key])) {
8422			return o[key];
8423		}
8424	}
8425	return "";//JSON.stringify(o);
8426};
8427
8428$utils.columnsMedia = ['image', 'audio', 'video', 'document'];
8429$utils.columnsButton = ['link', 'button'];
8430$utils.columnsMessage = ['blank', 'h1', 'h2', 'h3', 'h4', 'h5', 'message'];
8431$utils.columnsComponent = ['timetables', 'geo_schedule', 'categories', 'booking', 'buy', 'map', 'geo_properties', 
8432	'ui_relation', 'ui_image', 'ui_video', 'ui_audio', 'ui_document', 'any', 'group'];
8433
8434$utils.initColumns = function($scope, meta, columns, model, opts, extendedOpts, innerEntity) {
8435	columns = columns == null ? [] : columns;
8436	model = model == null ? {} : model;
8437	opts = angular.extend({}, opts, extendedOpts);
8438	
8439	opts.extra = opts.extra == null ? '' : opts.extra;
8440	opts.readonly = opts.readonly == null ? false : opts.readonly;
8441	opts.long = opts.long == null ? false : opts.long;
8442	opts.tab = opts.tab == null ? false : opts.tab;
8443	
8444	if (typeof meta == 'string') {
8445		meta = $entity.getEntity(meta);
8446		if (meta == null) {
8447			$angular.log('[initColumns] fatal error: entity not found');
8448		}
8449	}
8450
8451	if (opts.id == null && model.id) {
8452		opts.id = model.id;
8453	}
8454
8455	var pathToTranslations = 'geo_translations';
8456	if (innerEntity) {
8457		pathToTranslations += '.inner_entities.' + innerEntity;
8458	}
8459	
8460	var ereadonly = !opts.forceWritable && $entity.isReadonly(meta);
8461
8462	var cols = [];
8463	for (var k in columns) {
8464		var column = columns[k];
8465		if (column == null || column.name == null) {
8466			continue;
8467		}
8468		
8469		// clone column to avoid changes
8470		column = $utils.clone(column);
8471		columns[k] = column;
8472		
8473		if (column.super && !$user.isSuper()) {
8474			continue;
8475		}
8476		if (column.admin && !$user.isAdmin()) {
8477			continue;
8478		}
8479		if (column.unexposed) {
8480			continue;
8481		}
8482		if (column.hidden && $constants.isWeb) {
8483			continue;
8484		}
8485		if (column.tab && !opts.tab) {
8486			continue;
8487		}
8488		if (column.type == 'any' && !$user.isSuper()) {
8489			continue;
8490		}
8491		if (column.type == 'section') {
8492			column.type = 'h3';
8493		}
8494
8495		var isButton = $utils.columnsButton.indexOf(column.type) >= 0;
8496		var isMessage = $utils.columnsMessage.indexOf(column.type) >= 0;
8497		var isComponent = $utils.columnsComponent.indexOf(column.type) >= 0;
8498
8499		column._autosave = opts.id && column.autosave;
8500		column._readonly = false;
8501		if (!column._autosave && !isButton && !isMessage && !isComponent) {
8502			column._readonly = ereadonly || column.readonly || opts.readonly || ($constants.isWeb && !column.updatable);
8503		}
8504				
8505		var value = model[column.name];
8506
8507		// check if is array
8508		var isArray = false;
8509		switch (column.type) {
8510		case "image":
8511		case "audio":
8512		case "video":
8513		case "document":
8514		case "formula":
8515			if (column.type == 'formula') {
8516				if (column.formula) {
8517					// formula
8518					isArray = column.formula.contains('relation') && !column.formula.contains('correlation');
8519				}
8520				if (value == null && column.formula_type == 'boolean') {
8521					value = false;
8522				}
8523			} else {
8524				// media
8525				isArray = $utils.isTrue(column.multiple);
8526			}
8527			if (!isArray) {
8528				break;
8528 // skip check array
8529			}
8530		
8531		case "string_list":
8532		case "any_entity_list":
8533		case "list":
8534		case "relation":
8535		case "inner_entity_list":
8536		case "enum_list":
8537		case "ui_relation":
8538		case "ui_image":
8539		case "ui_video":
8540		case "ui_audio":
8541		case "ui_document":
8542		case "map":
8543		case "categories":
8544		case "geo_properties":
8545			if ($utils.isBlank(value)) {
8546				value = [];
8547			}
8548			if (!$utils.isArray(value)) {
8549				var valueIds = (typeof value === 'string') ? value.split(',') : null;
8550				if (valueIds && valueIds.every($utils.isGeoObjectId)) {
8551					value = valueIds;
8552				} else {
8553					value = [value];
8554				}
8555			}
8556			isArray = true;
8557			break;
8558		}
8559		
8560		// check data
8561		switch (column.type) {
8562		case "group":
8563			if (value == null) {
8564				value = {};
8565			}
8566			break;
8567		case "timetables":
8568			if (value != null && typeof value != 'string') {
8569				value = $utils.getTimeTables(value);
8570			}
8571			break;
8572		case "geo_schedule":
8573			if (value != null && typeof value != 'string') {
8574				value = $utils.printScheduledDates(value, 10);
8575			}
8576			break;
8577		case "booking":
8578			if (!$instance.isBooking(value)) {
8579				continue;
8580			}
8581			break;
8582		case "buy":
8583			if (!$cart.isCartItem(value)) {
8584				continue;
8585			}
8586			break;
8587		case "table":
8588			if (value == null) {
8589				value = [];
8590			}
8591			if ($utils.isArray(value) && column.columns != null && column.columns.length > 2) {
8592				var k1 = column.columns[0].name;
8593				var k2 = column.columns[1].name;
8594				
8595				// reset data
8596				$entity.initTable(column);
8597				
8598				var doc = {};
8599				for (var i in value) {
8600					var item = value[i];
8601					var v1 = item[k1];
8602					var v2 = item[k2];
8603					if (doc[v1] == null) {
8604						doc[v1] = {};
8605					}
8606					delete item[k1];
8607					delete item[k2];
8608					doc[v1][v2] = item;
8609				}
8610				
8611				value = doc;
8612				//console.log('table', doc);
8613			}
8614			break;
8615		case "button":
8616			column._buttonClass = column.style ? ('btn-' + column.style) : 'btn-primary';
8617			break;
8618		}
8619		
8620		model[column.name] = value; // assign if changed
8621		
8622		var hasData = !$utils.isBlank(value);
8623		if (hasData && isArray && value.length == 0) {
8624			hasData = false;
8625		}
8626		//console.log(column.name, column.type, value)
8627		 
8628		if (!hasData && opts.id == null && (column.type == 'formula' || column.type == 'relation_entity' || column.type == 'relation')) {
8629			continue; // no show in create
8630		}
8631		if (!hasData && isComponent) {
8632			continue; // no show components without data
8633		}
8634		if (column._readonly) {
8635			if (column.required && opts.id == null) {
8636				column._readonly = false;
8637				column.__readonly = true;
8638			} else if (!hasData) {
8639				continue;
8640			}
8641			if (column.type == 'formula' || column.type == 'relation_entity' || column.type == 'relation' || column.type == 'table' 
8642					|| column.type == 'inner_entity' || column.type == 'inner_entity_list') {
8643				column._readonly = false;
8644				column.__readonly = true;
8645			}
8646			if (column.type === 'list' && column.long) {
8647				column._readonly = false;
8648				column.__readonly = true;
8649			}
8650		}
8651		if (column.type == 'timetables' || column.type == 'geo_schedule' || column.type == 'categories') {
8652			column._readonly = true;
8653			column.html = true;	
8654		}
8655		
8656		if (column.tab) {
8657			column.long = true;
8658		}
8659		if (column.long || opts.long) {
8660			column.size = 12;
8661		}
8662		if (column.size == null) {
8663			column.size = 4;
8664		}
8665		
8666		column._type = column.type;
8667		if (column._readonly) {
8668			column._type = "readonly";
8669			column.required = false;
8670		}
8671		column._classes = "col-lg-" + column.size + " col-md-12 col-12 " + "form-group column";
8672		if (meta) {
8673			column._classes += " column-entity-" + meta.id;
8674		}
8675		if (column.type) {
8676			column._classes += " column-type-" + column.type;
8677		}
8678		if (column.name) {
8679			column._classes += " column-name-" + column.name;
8680		}
8681		if (column.required && !column.__readonly) {
8682			column._classes += " column-required";
8683		}
8684		if (column.classes) {
8685			column._classes += " " + column.classes;
8686		}
8687
8688		if (!column.nolabel && column.name.contains("_$_") && column.label == null) {
8689			column.nolabel = true;
8690		}
8691		
8692		if (isMessage && value != null) {
8693			column._label = $utils.parse(value);
8694		} else if (column.nolabel) {
8695			column._label = "";
8696			column._classes += " nolabel";
8697		} else if (column.label) {
8698			column._label = $utils.parse(column.label);
8699		} else if (column.name) {
8700			column._label = $utils.parseGeoTranslation(column.name, meta, null, pathToTranslations);
8701		}
8702
8703		if (column.type === 'boolean_int' && value === 1) {
8704			value = true;
8705			model[column.name] = value;
8706		}
8707
8708		if (column.calendar) {
8709			column._calendarConfig = {
8710				initialView: 'dayGridMonth',
8711				getItems: function() {
8712					var list = model[column.name];
8713					return list ? list : [];
8714				},
8715				onClickEvent: function(event, item) {
8716					var url = $angular.rootScope.getItemLinkBy(item);
8717					window.location.href = url;
8718				},
8719				convertItemToEvent: function(item) {
8720					var columns = $entity.getCalendarColumns(item.entity);
8721					var start;
8722					var end;
8723					var date = $utils.get(item, columns.date.name);
8724					if (columns.date.type === 'datetime') {
8725						start = date;
8726						end = date;
8727					} else {
8728						if (columns.time) {
8729							var time = $utils.get(item, columns.time.name);
8730							start = date + 'T' + time;
8731							end = date + 'T' + time.substring(0, 2) + ':59';
8732						} else {
8733							start = date;
8734							end = date;
8735						}
8736					}
8737
8738					var o = {
8739						id: item.id,
8740						start: start,
8741						end: end,
8742						title: $entity.displayColumn(item)
8743					};
8744					if (item.status) {
8745						o.classNames = ['fc-status-'+item.status];
8746					}
8747					return o;
8748				}
8749			};
8750		}
8751
8752		cols.push(column);
8753	}
8754	//console.log(entity, cols)
8755	//console.log(entity, model)
8756	
8757	$scope.meta = meta;
8758	$scope.columns = cols;
8759	$scope.model = $angular.contentScope.model;
8760	$scope._model = model;
8761	$scope.opts = opts;
8762	$scope.pathToTranslations = pathToTranslations;
8763	$scope.inner_entity = innerEntity;
8764};
8765
8766$utils.getGeoTranslation = function(field, o, type) {
8767
8768	if (!type) {
8769		type = 'geo_translations';
8770	}
8771	if (o == null && $angular.contentScope) {
8772		o = $angular.contentScope.meta;
8773
8774		if (o == null && $angular.contentScope.model) {
8775			o = $angular.contentScope.model.meta;
8776		}
8777	}
8778	if (typeof o == 'string') {
8779		o = $entity.getEntity(o);
8780	}
8781	if (o) {
8782		var v = $utils.get(o, type);
8783		if (v && v[field]) {
8784			return $utils.parse(v[field]);
8785		}
8786
8787		// TODO it can be inside an inner_entity -> search in object
8788		var ies = o.inner_entities;
8789		if (ies) {
8790			for (var k in ies) {
8791				var ie = ies[k];
8792				var ieo = o[ie.name];
8793				if (ieo && ieo[field]) {
8794					return $utils.parse(ieo[field]);
8795				}
8796			}
8797		}
8798
8799//		// try to fix column
8800//		var f = $utils.toKey(field);
8801//		v = o[type];
8802//		if (v && v[f]) {
8803//			return $utils.parse(v[f]);
8804//		}
8805
8806	}
8807	return null;
8808};
8809
8810$utils.parseGeoTranslation = function(field, o, msgKey, type) {
8811	var value = $utils.getGeoTranslation(field, o, type);
8812	if (!$utils.isBlank(value)) {
8813		return value;
8814	}
8815	if (msgKey == null) {
8816		msgKey = "msg." + field;
8817		msgKey = msgKey.trim();
8818	}
8819	var $translate = $angular.get("$translate");
8820	value = $translate.instant(msgKey);
8821	if (!$utils.isBlank(value) && value !== msgKey) {
8822		return value;
8823	}
8824	return $utils.toText(field);
8825};
8826$utils.parseText = function(field) {
8827	var msgKey = "msg." + field.trim();
8828	var $translate = $angular.get("$translate");
8829	var value = $translate.instant(msgKey);
8830	if (!$utils.isBlank(value) && value !== msgKey) {
8831		return value;
8832	}
8833	return $utils.toText(field);
8834};
8835
8836$utils.columnTypeText = function(type, placeholder, column) {
8837	if ($utils.isBlank(type)) {
8838		if (placeholder == true) {
8839			return "<font color='#777777'>Type</font>";
8840		}
8841		return "";
8842	}
8843	
8844	var extra = "";
8845	if (column && column.param) {
8846		extra = " (" + column.param + ")"; 
8847	}
8848	
8849	var key = "msg.column.type." + type;
8850	var $translate = $angular.get("$translate");
8851	var value = $translate.instant(key);
8852	if (!$utils.isBlank(value) && value !== key) {
8853		return value + extra;
8854	}
8855	return $utils.toText(type) + extra;
8856};
8857
8858$utils.length = function(value) {
8859	return $utils.keys(value).length;
8860};
8861
8862$utils.toText = function(field) {
8863	if (field != null && field.length > 1 && !$utils.isNumeric(field)) {
8864		if (field.match("_")) field = field.replaceAll("_", " ");
8865		if (field.match("-")) field = field.replaceAll("-", " ");
8866		field = field.trim();//.toLowerCase(); -> for text no translated (ex: '1. Solicitud')
8867		if (field.length > 1) {
8868			field = field.substring(0, 1).toUpperCase() + field.substring(1);
8869		}
8870	}
8871	return field;
8872};
8873
8874$utils.toKey = function(value) {
8875	if (value) {
8876		var key = value.toLowerCase().trim();
8877		key = key.replace(/[ ]/g, "-");
8878		key = $utils.removeDiacritics(key);
8879		key = key.replace(/[^\w-]/g, '');
8880		return key;
8881	}
8882	return "";
8883};
8884
8885$utils.isKeyOk = function(key) {
8886	if (key == null || key == "") {
8887		return false;
8888	}
8889	if ($constants.reservedWords.indexOf(key) != -1) {
8890		return false;
8891	}
8892	if (key.startsWith("admin")) {
8893		return false;
8894	}
8895	return true;
8896};
8897
8898$utils.getLocation = function(item) {
8899	var lat = item.latitude;
8900	if (lat == null) {
8901		lat = 0;
8902	}
8903	var lng = item.longitude;
8904	if (lng == null) {
8905		lng = 0;
8906	}
8907	return lat + "," + lng;
8908};
8909
8910$utils.getZoom = function(item) {
8911	var zoom = item.zoom;
8912	if (zoom == null) {
8913		zoom = 1;
8914	} else {
8915		zoom = Number(zoom);
8916	}
8917	return zoom;
8918};
8919
8920//$utils.getValueByPath = function(path) {
8921//	var val = $component.data;
8922//	var keys = path.split("/");
8923//	for (var index in keys) {
8924//		var key = keys[index];
8925//		if (val != null && key.length > 0) {
8926//			val = val[key];
8927//		}
8928//	}
8929//	val = $component.parse(val);
8930//	return val;
8931//};
8932//
8933//$utils.setValueByPath = function(path, value) {
8934//	var val = $component.data;
8935//	var keys = path.split("/");
8936//	for (var i=0; i<keys.length; i++) {
8937//		var key = keys[i];
8938//		if (val != null && key.length > 0) {
8939//			if (i == keys.length-1) {
8940//				// set value
8941//				val[key] = value;
8942//			} else {
8943//				val = val[key];
8944//			}
8945//		}
8946//	}
8947//};
8948
8949$utils.setByPath = function(o, path, value) {
8950	var split = path.split(".");
8951	for (var i in split) {
8952		var key = split[i];
8953		if (o && key.length > 0) {
8954			if (i == split.length-1) {
8955				// set value
8956				o[key] = value;
8957			} else {
8958				var tmp = o[key];
8959				if (tmp == null) {
8960					tmp = {};
8961					o[key] = tmp;				
8962				}
8963				o = tmp;
8964			}
8965		}
8966	}
8967};
8968
8969$utils.getVideoId = function(url) {
8970	var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
8971	var match = url.match(regExp);
8972	var videoId = (match && match[7].length == 11) ? match[7] : false;
8973	return videoId;
8974}
8975
8976$utils.isTabLoaded = function(value) {
8977	//console.log('isTabLoaded ' + value);
8978	$angular.contentScope.geoForm = null; // reset geoForm
8979
8980	$angular.get("$timeout")(function() {
8981
8982		// update current url with selected tab
8983		var index = $("#edit-tabs .nav-tabs li a.active").parent().attr("index");
8984		if (index == "-1") {
8985			return; // ignore
8986		}
8987		if (index && index != "0") {
8988			$angular.get("$location").search({'_tab': index}).replace();
8989		} else {
8990			$angular.get("$location").search({}).replace();
8991		}
8992
8993		$angular.contentScope.$broadcast('elastic:adjust');
8994		setTimeout(function() {
8995			$angular.contentScope.$broadcast('elastic:adjust');
8996		}, 300);
8997	});
8998
8999	if ($angular.contentScope.selectedTabs == undefined) {
9000		$angular.contentScope.selectedTabs = [];
9001	}
9002	if ($angular.contentScope.selectedTabs.indexOf(value) != -1) {
9003		return true;
9004	}
9005	$angular.contentScope.selectedTabs.push(value);
9006	return false;
9007};
9008$utils.isTabSelected = function(value) {
9009	if ($angular.contentScope.selectedTabs == null || $utils.isBlank(value)) {
9010		return false;
9011	}
9012	return $angular.contentScope.selectedTabs.indexOf(value) != -1;
9013};
9014
9015$utils.cloneList = function(list) {
9016	if (list == null) {
9017		return null;
9018	}
9019	return list.slice(0);
9020};
9021
9022$utils.getSorted = function(map, field) {
9023	var list = [];
9024	if (map) {
9025		for (var k in map) {
9026			list.push(map[k]);
9027		}
9028	}
9029	list.sort(function(a, b) {
9030		var numA = a[field];
9031		if (numA == null) numA = 999;
9032		var numB = b[field];
9033		if (numB == null) numB = 999;
9034		return numA - numB;
9035	});
9036	return list;
9037};
9038
9039$utils.imageSize = function(width, height) {
9040	if (width != "" && height != "") {
9041		width = Number(width);
9042		height = Number(height);
9043		var extra = "";
9044		return "padding-top:" + (height / width) * 100 + "%; " + extra;
9045	}
9046	return "";
9047};
9048
9049$utils.parseDateTotal = function(value) {
9050	if (value == null) {
9051		return "";
9052	}
9053	if ($utils.isArray(value)) {
9054		var list = [];
9055		for (var k in value) {
9056			list.push($utils.parseDateTotal(value[k]));
9057		}
9058		return $utils.join(list, ", ");
9059	}
9060	var isString = typeof value == 'string';
9061	if ($utils.isNumeric(value)) {
9062		var num = isString ? Number(value) : value;
9063		var d = new Date(num);
9064		if (d.getFullYear() > 1900) {
9065//			if ($angular.contentScope && $angular.contentScope.timezone != null && $angular.contentScope.timezone != 0) {
9066//				d = new Date(value + $angular.contentScope.timezone + (d.getTimezoneOffset()*60*1000))
9067//			}
9068//			if (millis) {
9069//				return $utils.fromDate(d, 'yyyy-MM-dd HH:mm:ss:sss');
9070//			}
9071			return $utils.fromDate(d, 'yyyy-MM-dd HH:mm:ss');
9072		}
9073	}
9074	if (isString && !value.contains('-') && !value.contains(' ')) {
9075		value = value.replace("+0000", "");
9076		var result = "";
9077		if (value.length > 7) {
9078			result = value.substring(0, 4) + "-" + value.substring(4, 6) + "-" + value.substring(6, 8);
9079		}
9080		if (value.length > 13) {
9081			result += " " + value.substring(8, 10) + ":" + value.substring(10, 12) + ":" + value.substring(12, 14);
9082		}
9083		return result;
9084	}
9085	return value;
9086};
9087$utils.fromDate = function(value, format, utc) {
9088	if (value != null && typeof value == 'object') {
9089		var $filter = $angular.get("$filter");
9090		return $filter('date')(value, format, utc);
9091	}
9092	return null;
9093};
9094$utils.toDate = function(value) {
9095	if (value) {
9096		if ($utils.isInteger(value)) {
9097			// millis
9098			return new Date(value);
9099		}
9100		if (typeof value == 'string') {
9101			
9102			if (value.length == 13 && $utils.isNumeric(value)) {
9103				// millis
9104				return new Date(Number(value));
9105			}
9106			
9107			if (value.length == 10) {
9108				// yyyy-MM-dd
9109				var split = value.split("-");
9110				if (split.length == 3) {
9111					//return new Date(value); -> this change the day result depending the timezone
9112					return new Date(Number(split[0]), Number(split[1]) - 1, Number(split[2]));
9113				}
9114
9115				// dd/MM/yyyy
9116				split = value.split("/");
9117				if (split.length == 3) {
9118					return new Date(Number(split[2]), Number(split[1]) - 1, Number(split[0])
9118);
9119				}
9120			}
9121			
9122			// dd MMM yyyy
9123			if (value.length == 11) {
9124				var split = value.split(" ");
9125				if (split.length == 3) {
9126					return new Date(value);
9127				}
9128			}
9129			
9130			if (value.length == 19) {
9131				// 2020-08-21T00:00:00
9132				var split = value.split("T");
9133				if (split.length == 2) {
9134					return new Date(value);
9135				}
9136				
9137				// 2020-10-02 19:09:53
9138				split = value.split(" ");
9139				if (split.length == 2) {
9140					return new Date(value);
9141				}
9142			}
9143			
9144		}
9145		//console.log('unknown date format: ' + value)
9146	}
9147	return null;
9148};
9149$utils.formatDate = function(value) {
9150	// used to parse form datetimes
9151	if (value && value.length == 19) {
9152		var date = $utils.toDate(value);
9153		if (date) {
9154			return date.getTime();
9155		}
9156	}
9157	return value;
9158};
9159$utils.parseDateTime = function(value) {
9160	// used to filter date times
9161	if (value) {
9162		var date = $utils.toDate(value);
9163		if (date) {
9164			return date.getTime();
9165		}
9166		//console.log('unknown date format: ' + value)
9167	}
9168	return null;
9169};
9170$utils.parseDay = function(value, format) {
9171	if (value == null || value === "0000-00-00") {
9172		return "";
9173	}
9174	var d = $utils.toDate(value);
9175	if (d != null) {
9176		if (format == null) {
9177			format = $constants.formatDay;
9178		}
9179		var result = $utils.fromDate(d, format);
9180		//console.log(value + " -> " + d + " -> " + result)
9181		if (result != null) {
9182			return result;
9183		}
9184	}
9185	return value;
9186};
9187$utils.toISODay = function(day) {
9188	var date = day;
9189	if (day && typeof day === 'string') {
9190		date = new Date(day);
9191	}
9192	var $filter = $angular.get("$filter");
9193	return $filter('date')(date, 'yyyy-MM-dd');
9194	// return date.getFullYear() + '-' + (date.getMonth()+1) + '-';
9195};
9196
9197$utils.printDayTime = function(dateInMillis) {
9198	return $utils.printDate(dateInMillis, $constants.formatDayTime);
9199};
9200
9201$utils.printDate = function(dateInMillis, format) {
9202	if (dateInMillis) {
9203		var date = new Date(dateInMillis);
9204		var $filter = $angular.get("$filter");
9205		return $filter('date')(date, format);
9206	}
9207	return null;
9208};
9209
9210$utils.getTimeZone = function() {
9211	var timeZone = "" + -(new Date().getTimezoneOffset() / 60);
9212	if (!timeZone.startsWith("-")) {
9213		timeZone = "+" + timeZone;
9214	}
9215	return timeZone;
9216};
9217$utils.getTimeZoneOffset = function() {
9218	return new Date().getTimezoneOffset();
9219};
9220
9221$utils.getTimeTables = function(list) {
9222	if (list == null || list.length == 0) {
9223		return '';
9224	}
9225	var result = "<table>";
9226	var $translate = $angular.get("$translate");
9227	for (var i in list) {
9228		var item = list[i];
9229		if (item && item.day) {
9230			result += "<tr><td>" + $translate.instant($constants.daysList[item.day])
9231				+ ": </td><td>" + item.time_from + " - " + item.time_to + "</td></tr>";
9232		}
9233	}
9234	result += "</table>";
9235	return result;
9236};
9237
9238$utils.hasValidScheduledDates = function(item) {
9239	var result = $utils.printScheduledDates(item);
9240	return !(result.startsWith("<font color='red'>"));
9241};
9242
9243$utils.printScheduledDates = function(item, max) {
9244	if (item && item.geo_schedule) {
9245		item = item.geo_schedule;
9246	}
9247	if (item == null || item.dates == null) {
9248		return '';
9249	}
9250	if (item.geo_scheduled_dates != null) {
9251		return item.geo_scheduled_dates;
9252	}
9253	var dates = item.dates;
9254	var result = '';
9255	var pastResult;
9256	var num = 0;
9257
9258	var timeZone = $utils.getTimeZone();
9259	var today = $utils.fromDate(new Date(), 'yyyy-MM-dd', 'UTC' + timeZone);
9260	var time = $utils.fromDate(new Date(), 'HH:mm', 'UTC' + timeZone);
9261	var oneYear = new Date();
9262	oneYear.setDate(oneYear.getDate() + 365);
9263
9264	if (max == null) {
9265		max = 1;
9266	}
9267	var table = false;
9268	if (max > 1) {
9269		table = true;
9270	}
9271
9272	var currentDay = null;
9273	for (var i in dates) {
9274		var date = dates[i];
9275		if (date && date.day) {
9276
9277			var allDay = !date.end_day && date.time_from === '00:00' && date.time_to === '23:59';
9278			var line = '<span class="scheduled_day">' + $utils.parseDay(date.day) + '</span>';
9279			if (!allDay) {
9280				line += '<span class="scheduled_time">' + date.time_from + '</span>' + '-';
9281				if (date.end_day) {
9282					line += '<span class="scheduled_day">' + $utils.parseDay(date.end_day) + '</span>';
9283				}
9284				line += '<span class="scheduled_time">' + date.time_to + '</span>';
9285			}
9286
9287			if (date.day > oneYear) {
9288				continue;
9289			}
9290			var endDay = date.end_day ? date.end_day : date.day;
9291			if (endDay < today) {
9292				pastResult = line;
9293				continue;
9294			}
9295			if (endDay === today && date.time_to < time) {
9296				pastResult = line;
9297				continue;
9298			}
9299
9300			if (!table && date.day === currentDay && !date.end_day && !allDay) {
9301				result += ' / ' + '<span class="scheduled_time">' + date.time_from + '</span>' + '-' + '<span class="scheduled_time">' + date.time_to + '</span>';
9302				continue;
9303			}
9304			currentDay = date.day;
9305
9306			if (num === max) {
9307				break;
9308			}
9309
9310			result += line
9311			if (table) {
9312				result += '<br>';
9313			} else if (allDay) {
9314				break;
9315			}
9316
9317			num++;
9318		}
9319	}
9320
9321	if (table) {
9322		result = num === 0 ? '' : ('<div class="scheduled_dates">' + result + '</div>');
9323	} else if (num === 0 && pastResult) {
9324		result = "<font color='red'>" + pastResult + "</font>";
9325	}
9326
9327	item.geo_scheduled_dates = result;
9328	return result;
9329};
9330
9331$utils.parseTimetables = function(list) {
9332	var result = "";
9333	if (list) {
9334		var $translate = $angular.get("$translate");
9335		for (var i in list) {
9336			var item = list[i];
9337			if (item && item.day) {
9338				if (result != "") {
9339					result += "<br>";
9340				}
9341				result += $translate.instant($constants.daysList[item.day]) + ": " + item.time_from + " - " + item.time_to;
9342			}
9343		}
9344	}
9345	return result;
9346};
9347
9348$utils.parseDuration = function(value) {
9349	if (value == null || value < 30000) {
9350		return "-";
9351	}
9352	var sec_num = Math.round(parseInt(value, 10) / 1000);
9353    var hours   = Math.floor(sec_num / 3600);
9354    var minutes = Math.round((sec_num - (hours * 3600)) / 60);
9355
9356    var result = '';
9357    if (hours > 0) {
9358    	result += hours +'h ';
9359    }
9360    //if (minutes > 0) {
9361    	if (minutes < 10 && hours > 0) {
9362    		result += "0";
9363    	}
9364    	result += minutes +'m ';
9365    //}
9366    if (result == '') {
9367    	return "-";
9368    }
9369    return result;
9370};
9371$utils.parseNumber = function(value) {
9372	if (value != null) {
9373		var miles = ($locale.decimal == ",") ? "." : ",";
9374		var val = value.toString().replace(".", $locale.decimal);
9375		if (value > 999) {
9376			val = val.replace(/\B(?=(\d{3})+(?!\d))/g, miles);
9377		}
9378		return val;
9379	}
9380	return "";
9381};
9382//$utils.parseNumber = function(value) {
9383//	if (value != null) {
9384//		//return value.toLocaleString();
9385//		return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
9386//	}
9387//	return "";
9388//};
9389//$utils.parseDecimal = function(value) {
9390//	if (value != null) {
9391//		value = (Math.round(value * 100) / 100).toFixed(2);
9392//		var split = value.split(".");
9393//		return $utils.parseNumber(split[0]) + "," + split[1];
9394//	}
9395//	return "";
9396//};
9397$utils.parsePercent = function(value) {
9398	if (value != null) {
9399		return value + "%";
9400	}
9401	return "0%";
9402};
9403
9404$utils.isColorDark = function(color) {
9405
9406	if (color == null || color == "") {
9407		return true;
9408	}
9409
9410	var c = color.toUpperCase();
9411	if (c.startsWith("#")) {
9412		c = c.substring(1); // strip #	
9413	}
9414	var rgb = parseInt(c, 16);   // convert rrggbb to decimal
9415	var r = (rgb >> 16) & 0xff;  // extract red
9416	var g = (rgb >> 8) & 0xff;  // extract green
9417	var b = (rgb >> 0) & 0xff;  // extract blue
9418	var luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709
9419	// 255:white / 0:black
9420	//console.log(c + " -> " + luma)
9421	if (luma < 190) { // default: 220
9422		return true;
9423	} else {
9424		return false;
9425	}
9426};
9427$utils.getColorText = function(color) {
9428	if ($utils.isColorDark(color)) {
9429		return "#ffffff";
9430	}
9431	return "#000000";
9432};
9433
9434$utils.getParameterFromUrl = function(name, url) {
9435	if (url == undefined) {
9436		url = "" + window.location.href;
9437	}
9438
9439	name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
9440	var regexS = "[\\?&]" + name + "=([^&#]*)";
9441	var regex = new RegExp(regexS);
9442	var results = regex.exec(url);
9443	if (results == null) {
9444		return "";
9445	} else {
9446		return results[1];
9447	}
9448};
9449
9450$utils.removeFromArray = function(list, value) {
9451	var index = list.indexOf(value);
9452	if (index > -1) {
9453		list.splice(index, 1);
9454	}
9455}
9456
9457$utils.isBlank = function(value) {
9458//	if (value == null) return true;
9459	//if ($utils.isNumeric(value)) return false;
9460//	if (typeof value == "string") {
9461//		return value.trim() == "";
9462//	}
9463//	return false;
9464	return value == null || value === ''; // || value == "undefined" || value == "null";
9465};
9466
9467$utils.isEmpty = function(value) {
9468	if ($utils.isArray(value)) {
9469		return value.length === 0;
9470	} if ($utils.isObject(value)) {
9471		return Object.keys(value).length === 0;
9472	}
9473	return $utils.isBlank(value);
9474};
9475
9476$utils.isNotEmpty = function(value) {
9477	return !$utils.isEmpty(value);
9478};
9479
9480$utils.isTrue = function(value) {
9481	if (value != null) {
9482		return (value === 'true' || value === true || value === 1);
9483	}
9484	return false;
9485}
9486
9487$utils.isNumeric = function(n) {
9488	return !isNaN(parseFloat(n)) && isFinite(n);
9489}
9490
9491$utils.isInteger = function(value) {
9492	return typeof value === "number" &&
9493		isFinite(value) &&
9494		Math.floor(value) === value;
9495}
9496
9497$utils.toNumber = function(value) {
9498	return Number(value);
9499};
9500
9501$utils.isId = function(o) {
9502	var re = /^[a-zA-Z0-9 ]+$/;
9503	return !re.test(o);
9504};
9505
9506$utils.keys = function(obj) {
9507	if (obj != undefined) {
9508		return Object.keys(obj);
9509	}
9510	return [];
9511};
9512$utils.sortKeys = function(o) {
9513	if (o == null) {
9514		return null;	
9515	}
9516	delete o["$$hashKey"];
9517
9518	// sort object by key
9519	var keys = $utils.keys(o);
9520	keys.sort();
9521	var a = {};
9522	for (var i in keys) {
9523		var k = keys[i];
9524		a[k] = o[k];
9525		
9526		if ($utils.isObject(a[k])) {
9527			a[k] = $utils.sortKeys(a[k]);
9528		} 
9529	}
9530	
9531	return a;
9532};
9533
9534$utils.firstkey = function(obj) {
9535	if (obj != undefined) {
9536		var keys = Object.keys(obj);
9537		if (keys.length > 0) {
9538			return keys[0];
9539		}
9540	}
9541	return "";
9542};
9543
9544$utils.keyToName = function(key) {
9545	var name = "";
9546	if (key != undefined) {
9547		var split = key.split("-");
9548		for (var index in split) {
9549			if (name != "") {
9550				name += " ";
9551			}
9552			name += $utils.firstUpperCase(split[index]);
9553		}
9554	}
9555	return name;
9556};
9557
9558$utils.firstUpperCase = function(name) {
9559	if (name != undefined) {
9560		if (name.length < 2) {
9561			return name;
9562		}
9563		return name.substring(0, 1).toUpperCase() + name.substring(1);
9564	}
9565	return "";
9566};
9567
9568$utils.join = function(list, separator) {
9569	var value = "";
9570	if (list) {
9571		if ($utils.isArray(list)) {
9572			for (var key in list) {
9573				var item = list[key];
9574				if (value != "") {
9575					value += separator;
9576				}
9577				value += item;
9578			}
9579		} else {
9580			for (var key in list) {
9581				var item = list[key];
9582				if (value != "") {
9583					value += separator;
9584				}
9585				value += key + "=" + item;
9586			}
9587		}
9588	}
9589	return value;
9590};
9591
9592$utils.parseTags = function(list) {
9593	var value = "";
9594	if (list) {
9595		for (var key in list) {
9596			var item = list[key];
9597			if (value != "") {
9598				value += ",";
9599			}
9600			if (item.text) {
9601				item = item.text;
9602			}
9603			value += item;
9604		}
9605	}
9606	return value;
9607};
9608
9609$utils.getPathBy = function(path, part) {
9610	var i = path.indexOf(part);
9611	return path.substring(0, i) + part;
9612};
9613
9614$utils.validateAlphanumeric = function(name) {
9615	if (name == null || name.trim() == "") {
9616		throw "value cannot be empty";
9617	}
9618	var re = /^[a-zA-Z0-9 ]+$/;
9619	if (!re.test(name)) {
9620		throw "'" + name + "' contains invalid characters";
9621	}
9622	var INVALID_KEYS = ["entity", "test"];
9623	if (INVALID_KEYS.indexOf(name.toLowerCase()) != -1) {
9624		throw "'" + name + "' is not valid";
9625	}
9626};
9627
9628$utils.getRandom = function() {
9629	return (Math.random() * 11 * 10000).toFixed(0);
9630};
9631
9632$utils.showLoading = function(field) {
9633	if (field && field != "generic") {
9634		field = $(field);
9635		if (field.length > 0) {
9636			// loading in place
9637			if (field.find("label.loading").length == 0) {
9638
9639				// Buttons bootstrap
9640				if (field.find("> span.material-icons").length > 0) {
9641					field.find("> span.material-icons").addClass("d-none").after('<label class="loading">&nbsp;</label>');
9642
9643				} else {
9644					field.append('<label class="loading">&nbsp;</label>');
9645				}
9646			}
9647			return;
9648		}
9649	}
9650
9651	// generic loading
9652	if ($("#full-loading").length == 0) {
9653		//console.log(new Error('showing loading'))
9654		$("body").append($('<div id="full-loading"></div>'));
9655		$("body").addClass('full-loading');
9656	}
9657};
9658
9659$utils.hideLoading = function(field) {
9660	if ($("#full-loading").length > 0) {
9661		//console.log(new Error('hiding loading'))
9662		$("#full-loading").remove();
9663		$("body").removeClass('full-loading');
9664	}
9665	if (field && field != "generic" && $(field).length > 0) {
9666		field = $(field);
9667		field.find("label.loading").remove();
9668
9669
9670		// Buttons bootstrap
9671		if (field.find("span.material-icons").length > 0) {
9672			field.find("span.material-icons").removeClass("d-none");
9673		}
9674	}
9675};
9676
9677/** Parse the javascript number to string */
9678$utils.numberParse = function(value, numDecimals, decimales) {
9679	return value;
9680// TODO for negative values
9681//	var miles = ",";
9682//	if (decimales == undefined || decimales == "") {
9683//		decimales = $locale.decimal;
9684//	}
9685//	if (decimales == ",") {
9686//		miles = ".";
9687//	} else {
9688//		decimales = ".";
9689//	}
9690//	if (numDecimals == undefined || numDecimals == "") {
9691//		numDecimals = 2;
9692//	}
9693//	
9694//	value = value.toFixed(numDecimals);
9695//	
9696//	var str = value + '';
9697//	var decimal = str.split(".")[1];
9698//	if (decimal.length == 0) {
9699//		decimal = decimales + "00";
9700//	} else if (decimal.length == 1) {
9701//		decimal = decimales + decimal+"0";
9702//	} else {
9703//		decimal = decimales + decimal;
9704//	}
9705//	
9706//	str = str.split(".")[0];
9707//	
9708//	var amount = new String(str);
9709//    amount = amount.split("").reverse();
9710//
9711//    var output = "";
9712//    for ( var i = 0; i <= amount.length-1; i++ ){
9713//        output = amount[i] + output;
9714//        if ((i+1) % 3 == 0 && (amount.length-1) !== i)output = miles + output;
9715//    }
9716//    return output + decimal;
9717};
9718
9719/** Parse the string to javascript number */
9720$utils.numberFormat = function(valor, decimales) {
9721
9722	var miles = ",";
9723	if (decimales == undefined || decimales == "") {
9724		decimales = $locale.decimal;
9725	}
9726	if (decimales == ",") {
9727		miles = ".";
9728	}
9729
9730	// Eliminamos separador de miles
9731	var resultado = valor.replace(miles, "");
9732
9733	// Reemplazmos el separador de decimles
9734	resultado = resultado.replace(decimales, ".");
9735
9736	// Se convierte en number
9737	resultado = Number(resultado);
9738
9739	return resultado;
9740};
9741
9742$utils.getAgeFromDate = function(date) {
9743	if (date.length >= 8) {
9744		var fechaIngresada = new Date(date.substring(0, 4),
9745			date.substring(4, 6), date.substring(6, 8));
9746		var fechaActual = new Date();
9747		var years = new Date(fechaActual.getTime() - fechaIngresada.getTime());
9748		var base = new Date(0);
9749		return years.getFullYear() - base.getFullYear();
9750	}
9751	return -1;
9752};
9753
9754$utils.lowerCase = function(name) {
9755	return name.toLowerCase();
9756};
9757
9758$utils.upperCase = function(name) {
9759	return name.toUpperCase();
9760};
9761
9762//// slash to camel case
9763//$utils.camelCase = function(name) {
9764//	return name.toUpperCase();
9765//};
9766//// camel to slash case
9767//$utils.slashCase = function(name) {
9768//	return name.toUpperCase();
9769//};
9770
9771$utils.capitalize = function(string) {
9772	return string.charAt(0).toUpperCase() + string.slice(1);
9773};
9774
9775$utils.floatingError = function(msg) {
9776	$utils.floatingMsg(msg, {state: 'danger', time: 6000});
9777};
9778
9779$utils.floatingWarn = function(msg) {
9780	$utils.floatingMsg(msg, {state: 'info', time: 6000});
9781};
9782
9783$utils.floatingMsg = function($msg, options) {
9784
9785	//console.error('showing ' + $msg)
9786
9787	var defaults = {
9788		time: 5000,
9789		afterFunction: null,
9790		state: 'success'
9791	};
9792
9793	if (options == undefined) {
9794		options = {};
9795	}
9796
9797	var _settings = $.extend({}, defaults, options);
9798
9799	var $container = $('#fmc');
9800	if ($container.length == 0) {
9801		$container = $('<div id="fmc"></div>');
9802		$('body').append($container);
9803	}
9804
9805	// remove old
9806	$("div.floating-msg").remove();
9807
9808	var $div = $('<div class="alert alert-' + _settings.state + ' floating-msg"></div>');
9809	$div.append('<span class="close material-icons">close</span> ');
9810	$div.css('display', 'none');
9811	$div.append($msg);
9812	$container.append($div);
9813	$div.fadeIn("fast");
9814
9815	var closeFn = function() {
9816		$div.fadeOut("slow", function() {
9817			$div.remove();
9818		});
9819		if (_settings.afterFunction != null) {
9820			_settings.afterFunction();
9821		}
9822	};
9823
9824	$div.find("span.close").click(closeFn);
9825
9826	setTimeout(closeFn, _settings.time);
9827};
9828
9829$utils.closeFloating = function() {
9830	if ($(".alert.floating-msg").length > 0) {
9831		$(".alert.floating-msg .close").click();
9832	}
9833}
9834
9835$utils.showAlert = function(msg) {
9836	$angular.apply($angular.rootScope, function() {
9837		$("#content > *").remove();
9838		$("#content").html(
9839			'<div id="messages" class="alert alert-danger"><span class="material-icons">warning</span> <label>' + msg + '</label></div>'
9840			+ '<a id="btnBack" href="javascript:void(0)" onclick="$angular.goHome()" class="btn btn-primary">' + $angular.get("$translate").instant("msg.home") + '</a>');
9841	});
9842};
9843$utils.showAccessDenied = function() {
9844	$utils.showAlert($angular.get("$translate").instant("msg.accessDenied"));
9845};
9846$utils.showNotFound = function() {
9847	$utils.showAlert($angular.get("$translate").instant("msg.notFound"));
9848};
9849$utils.showNoInternet = function() {
9850	$utils.showAlert($angular.get("$translate").instant("msg.error.noConnection"));
9851};
9852$utils.showError = function() {
9853	$utils.showAlert($angular.get("$translate").instant("msg.error"));
9854};
9855
9856
9857$utils.dataURItoBlob = function(dataURI) {
9858	// convert base64 to raw binary data held in a string
9859	// doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
9860	var byteString = atob(dataURI.split(',')[1]);
9861
9862	// separate out the mime component
9863	var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
9864
9865	// write the bytes of the string to an ArrayBuffer
9866	var ab = new ArrayBuffer(byteString.length);
9867	var ia = new Uint8Array(ab);
9868	for (var i = 0; i < byteString.length; i++) {
9869		ia[i] = byteString.charCodeAt(i);
9870	}
9871
9872	//Old Code
9873	//write the ArrayBuffer to a blob, and you're done
9874	//var bb = new BlobBuilder();
9875	//bb.append(ab);
9876	//return bb.getBlob(mimeString);
9877
9878	//New Code
9879	return new Blob([ab], {type: mimeString});
9880};
9881
9882$utils.defaultDiacriticsRemovalMap = [
9883	{
9884		'base': 'A',
9885		'letters': '\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F'
9886	},
9887	{'base': 'AA', 'letters': '\uA732'},
9888	{'base': 'AE', 'letters': '\u00C6\u01FC\u01E2'},
9889	{'base': 'AO', 'letters': '\uA734'},
9890	{'base': 'AU', 'letters': '\uA736'},
9891	{'base': 'AV', 'letters': '\uA738\uA73A'},
9892	{'base': 'AY', 'letters': '\uA73C'},
9893	{'base': 'B', 'letters': '\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181'},
9894	{'base': 'C', 'letters': '\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E'},
9895	{
9896		'base': 'D',
9897		'letters': '\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779\u00D0'
9898	},
9899	{'base': 'DZ', 'letters': '\u01F1\u01C4'},
9900	{'base': 'Dz', 'letters': '\u01F2\u01C5'},
9901	{
9902		'base': 'E',
9903		'letters': '\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E'
9904	},
9905	{'base': 'F', 'letters': '\u0046\u24BB\uFF26\u1E1E\u0191\uA77B'},
9906	{
9907		'base': 'G',
9908		'letters': '\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E'
9909	},
9910	{'base': 'H', 'letters': '\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D'},
9911	{
9912		'base': 'I',
9913		'letters': '\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197'
9914	},
9915	{'base': 'J', 'letters': '\u004A\u24BF\uFF2A\u0134\u0248'},
9916	{'base': 'K', 'letters': '\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2'},
9917	{
9918		'base': 'L',
9919		'letters': '\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u014
99191\u023D\u2C62\u2C60\uA748\uA746\uA780'
9920	},
9921	{'base': 'LJ', 'letters': '\u01C7'},
9922	{'base': 'Lj', 'letters': '\u01C8'},
9923	{'base': 'M', 'letters': '\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C'},
9924	{
9925		'base': 'N',
9926		'letters': '\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4'
9927	},
9928	{'base': 'NJ', 'letters': '\u01CA'},
9929	{'base': 'Nj', 'letters': '\u01CB'},
9930	{
9931		'base': 'O',
9932		'letters': '\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C'
9933	},
9934	{'base': 'OI', 'letters': '\u01A2'},
9935	{'base': 'OO', 'letters': '\uA74E'},
9936	{'base': 'OU', 'letters': '\u0222'},
9937	{'base': 'OE', 'letters': '\u008C\u0152'},
9938	{'base': 'oe', 'letters': '\u009C\u0153'},
9939	{'base': 'P', 'letters': '\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754'},
9940	{'base': 'Q', 'letters': '\u0051\u24C6\uFF31\uA756\uA758\u024A'},
9941	{
9942		'base': 'R',
9943		'letters': '\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782'
9944	},
9945	{
9946		'base': 'S',
9947		'letters': '\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784'
9948	},
9949	{
9950		'base': 'T',
9951		'letters': '\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786'
9952	},
9953	{'base': 'TZ', 'letters': '\uA728'},
9954	{
9955		'base': 'U',
9956		'letters': '\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244'
9957	},
9958	{'base': 'V', 'letters': '\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245'},
9959	{'base': 'VY', 'letters': '\uA760'},
9960	{'base': 'W', 'letters': '\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72'},
9961	{'base': 'X', 'letters': '\u0058\u24CD\uFF38\u1E8A\u1E8C'},
9962	{
9963		'base': 'Y',
9964		'letters': '\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE'
9965	},
9966	{'base': 'Z', 'letters': '\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762'},
9967	{
9968		'base': 'a',
9969		'letters': '\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250'
9970	},
9971	{'base': 'aa', 'letters': '\uA733'},
9972	{'base': 'ae', 'letters': '\u00E6\u01FD\u01E3'},
9973	{'base': 'ao', 'letters': '\uA735'},
9974	{'base': 'au', 'letters': '\uA737'},
9975	{'base': 'av', 'letters': '\uA739\uA73B'},
9976	{'base': 'ay', 'letters': '\uA73D'},
9977	{'base': 'b', 'letters': '\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253'},
9978	{'base': 'c', 'letters': '\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184'},
9979	{'base': 'd', 'letters': '\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A'},
9980	{'base': 'dz', 'letters': '\u01F3\u01C6'},
9981	{
9982		'base': 'e',
9983		'letters': '\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD'
9984	},
9985	{'base': 'f', 'letters': '\u0066\u24D5\uFF46\u1E1F\u0192\uA77C'},
9986	{
9987		'base': 'g',
9988		'letters': '\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F'
9989	},
9990	{
9991		'base': 'h',
9992		'letters': '\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265'
9993	},
9994	{'base': 'hv', 'letters': '\u0195'},
9995	{
9996		'base': 'i',
9997		'letters': '\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131'
9998	},
9999	{'base': 'j', 'letters': '\u006A\u24D9\uFF4A\u0135\u01F0\u0249'},
10000	{'base': 'k', 'letters': '\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3'},
10001	{
10002		'base': 'l',
10003		'letters': '\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747'
10004	},
10005	{'base': 'lj', 'letters': '\u01C9'},
10006	{'base': 'm', 'letters': '\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F'},
10007	{
10008		'base': 'n',
10009		'letters': '\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5'
10010	},
10011	{'base': 'nj', 'letters': '\u01CC'},
10012	{
10013		'base': 'o',
10014		'letters': '\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275'
10015	},
10016	{'base': 'oi', 'letters': '\u01A3'},
10017	{'base': 'ou', 'letters': '\u0223'},
10018	{'base': 'oo', 'letters': '\uA74F'},
10019	{'base': 'p', 'letters': '\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755'},
10020	{'base': 'q', 'letters': '\u0071\u24E0\uFF51\u024B\uA757\uA759'},
10021	{
10022		'base': 'r',
10023		'letters': '\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783'
10024	},
10025	{
10026		'base': 's',
10027		'letters': '\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B'
10028	},
10029	{
10030		'base': 't',
10031		'letters': '\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787'
10032	},
10033	{'base': 'tz', 'letters': '\uA729'},
10034	{
10035		'base': 'u',
10036		'letters': '\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289'
10037	},
10038	{'base': 'v', 'letters': '\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C'},
10039	{'base': 'vy', 'letters': '\uA761'},
10040	{'base': 'w', 'letters': '\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73'},
10041	{'base': 'x', 'letters': '\u0078\u24E7\uFF58\u1E8B\u1E8D'},
10042	{
10043		'base': 'y',
10044		'letters': '\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF'
10045	},
10046	{'base': 'z', 'letters': '\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763'}
10047];
10048
10049$utils.diacriticsMap = {};
10050for (var i = 0; i < $utils.defaultDiacriticsRemovalMap.length; i++) {
10051	var letters = $utils.defaultDiacriticsRemovalMap[i].letters;
10052	for (var j = 0; j < letters.length; j++) {
10053		$utils.diacriticsMap[letters[j]] = $utils.defaultDiacriticsRemovalMap[i].base;
10054	}
10055}
10056
10057// "what?" version ... http://jsperf.com/diacritics/12
10058$utils.removeDiacritics = function(str) {
10059	return str.replace(/[^\u0000-\u007E]/g, function(a) {
10060		return $utils.diacriticsMap[a] || a;
10061	});
10062};
10063
10064$utils.loadFile = function(filetype, filePath) {
10065
10066	var hc = filePath.hashCode();
10067	$("#f" + hc).remove();
10068
10069	if (filetype == "js") {
10070		var fileref = document.createElement('script')
10071		fileref.setAttribute("type", "text/javascript")
10072		fileref.setAttribute("src", filePath)
10073		fileref.setAttribute("id", "f" + hc)
10074		document.getElementsByTagName("head")[0].appendChild(fileref)
10075
10076	} else if (filetype == "css") {
10077		var fileref = document.createElement("link")
10078		fileref.setAttribute("rel", "stylesheet")
10079		fileref.setAttribute("type", "text/css")
10080		fileref.setAttribute("href", filePath)
10081		fileref.setAttribute("id", "f" + hc)
10082		document.getElementsByTagName("head")[0].appendChild(fileref)
10083	}
10084};
10085
10086$utils.isEmail = function(email) {
10087	var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
10088	return re.test(String(email).toLowerCase());
10089}
10090
10091$utils.parseToHtml = function(o, name, level) {
10092	if (o == null) {
10093		return "";
10094	}
10095	if (level == null) {
10096		level = 0;
10097	}
10098	if (name == null) {
10099		name = "";
10100	}
10101
10102	if ($utils.isArray(o)) {
10103		if (o.length == 0) {
10104			return "";
10105		}
10106		if (o.length == 1) {
10107			return $utils.parseToHtml(o[0]);
10108		}
10109
10110		var result = "";
10111		level = 0; // reset with ul
10112		result += "<ul class='list-group'>";
10113		for (var key in o) {
10114			var item = o[key];
10115			
10116			if (item.entity && item.id) {
10117				
10118				var url = $angular.rootScope.getItemLinkBy(item);
10119				result += "<li class='list-group-item table-relation'>";
10120				result += "<a class='row' href='" + url + "'>";
10121				var column = $entity.getColumn(item.entity, name);
10122				var cols = $entity.getTableColumns(item.entity, column);
10123				if (cols.length > 1 && column == null) {
10124					cols = [cols[0]]; // just first
10125				}
10126				for (var k in cols) {
10127					var col = cols[k];
10128					result += "<div class='col-sm col-6'>";
10129					if (cols.length > 1) {
10130						result += "<small>"+$utils.parseGeoTranslation(col.name, item.entity)+"</small>";
10131					}
10132					result += "<label>"+$entity.displayColumn(item, col)+"</label></div>";
10133				}
10134				result += "</a>";
10135				result += "</li>";
10136			
10137			} else {
10138				result += "<li class='list-group-item'>";
10139				var val = $utils.parseToHtml(item, name, level);
10140				if (name == 'categories') {
10141					val = $constants.getEntityDesc('category', val);
10142				}
10143				result += val + "</li>";
10144			}
10145		}
10146		result += "</ul>";
10147		return result;
10148	}
10149	
10150	if (typeof (o) == 'object') {
10151		
10152		if (o.entity && o.id) {
10153			var val = $entity.displayColumn(o);
10154			if (val) {
10155				var url = $angular.rootScope.getItemLinkBy(o);
10156				if (url != null) {
10157					return "<a href='" + url + "'>" + val + "</a>";
10158				}
10159				return val;
10160			}
10161		}
10162
10163		if (o.day != null && o.time_from != null && o.time_to != null) {
10164			return o.day + " " + o.time_from + "-" + o.time_to;
10165		}		
10166		
10167		var otmp = {};
10168		for (var key in o) {
10169			var val = o[key];
10170			if (val == null || key == '$$hashKey') {
10171				continue;
10172			}
10173			
10174			var lev = level;
10175			if (typeof (val) == 'object') {
10176				lev++;
10177			}
10178			
10179			var text = $utils.toText(key);
10180			
10181			switch (key) {
10182			case "id": 
10183				continue;
10184				
10185			case "entity":
10186			case "entityType":
10187			case "entityId":
10188			case "entity_id":
10189				var ent = o.entity != null ? o.entity : o.entityType;
10190				var eid = o.entityId != null ? o.entityId : o.entity_id;
10191				if (ent != null || eid != null) {
10192					text = "Entity";
10193					var url = $angular.rootScope.getItemLink(ent, eid);
10194					val = "<a href='" + url + "'>" + $constants.getEntityDesc(ent, eid) + "</a>";
10195				}
10196				break;
10197
10198			case "action":
10199			case "value":
10200				if (o.action && o.value) {
10201					var userAction = $constants.userActions[o.action];
10202					if (userAction) {
10203						userAction = $angular.get("$translate").instant(userAction);
10204					} else {
10205						userAction = o.action;
10206					}
10207					text = "Action";
10208					val = $utils.parseToHtml(o.value, 'action', lev + 1);
10209				}
10210				break;
10211				
10212			case "geo_location":
10213			case "latitude":
10214			case "longitude":
10215				var tmp = o;
10216				if (tmp.geo_location) {
10217					tmp = tmp.geo_location;
10218				}
10219				if (tmp.latitude && tmp.longitude) {
10220					var loc = tmp.latitude + "," + tmp.longitude;
10221					var url = "https://www.google.com/maps/search/?api=1&query=" + loc;
10222					text = "Location";
10223					val = "<a href='" + url + "' target='_blank'>" + loc + "</a>";
10224				}
10225				break;
10226				
10227			case "device":
10228				val = "<a href='" + $instance.getBaseUrl() + "device/" + val + "'>" + $constants.getEntityDesc('device', val) + "</a>";
10229				break;	
10230
10231			case "instance":
10232				val = "<a href='" + $instance.getBaseUrl() + "instance/" + val + "'>" + $constants.getEntityDesc('instance', val) + "</a> ";
10233				break;	
10234				
10235			case "date":
10236				val = $utils.parseDateTotal(val);
10237				break;	
10238				
10239			default:
10240				var value = $utils.parseToHtml(val, key, lev);
10241				 if (!$utils.isBlank(value)) {
10242					val = value;
10243				}
10244				break;
10245			}
10246			otmp[text] = val;
10247		}
10248		
10249		var result = "";
10250		for (var key in otmp) {
10251			result = $utils.addBrIfNeeded(result);
10252			result += key + ": " + otmp[key] + " ";
10253		}
10254		return "<div class='action-" + level + "'>" + result + "</div>";
10255	}
10256
10257	if (typeof o == 'string') {
10258		
10259		if (o.startsWith('http')) {
10260			return "<a href='" + o + "' target='_blank'>" + o + "</a>";
10261		}
10262		
10263		if ($utils.isEmail(o)) {
10264			var url = $angular.rootScope.getItemLink('user', o);
10265			return "<a href='" + url + "'>" + o + "</a>";
10266		}
10267		
10268		var split = o.split('#|#');
10269		if (split.length === 2) {
10270			var url = $angular.rootScope.getItemLink(split[0], split[1]);
10271			return "<a href='" + url + "'>" + split[0] + ":" + split[1] + "</a>";
10272		}
10273	}
10274
10275	return o;
10276};
10277
10278$utils.addBrIfNeeded = function(result) {
10279	result = result.trim();
10280	if (result.length > 8) {
10281		//console.log(result.endsWith('</div>') + " -> " + result.substring(result.length-8), result.length)
10282		if (!result.endsWith('</div>')) {
10283			result += "<br>";
10284		}
10285	}
10286	return result;
10287};
10288
10289$utils.printCurrency = function(price, currency) {
10290	if (!price || isNaN(price)) {
10291		price = 0;
10292	}
10293	if (!currency && $instance.current && $instance.current.geo_buy) {
10294		currency = $instance.current.geo_buy.currency;
10295	}
10296	var config = null;
10297	if ($angular.currencies && currency) {
10298		config = $angular.currencies[currency];
10299	}
10300	if (config == null) {
10301		if (currency == null) {
10302			currency = "";
10303		}
10304		config = {
10305			decimalDigits: 2,
10306			symbolAfterNumber: true,
10307			symbol: currency
10308		};
10309	}
10310	
10311	var isNegative = price < 0;
10312	if (isNegative) {
10313		price = -price;
10314		}
10315	
10316	var print = config.symbolAfterNumber ? '' : config.symbol;
10317	print += $utils.parseNumber(Number(price).toFixed(config.decimalDigits));
10318	print += config.symbolAfterNumber ? ' ' + config.symbol : '';
10319	
10320	if (isNegative) {
10321		print = "-" + print;
10322	}
10323	
10324	return print;
10325};
10326
10327$utils.addTaxes = function(price, tax) {
10328	if (!price || isNaN(price)) {
10329		price = 0;
10330	}
10331	price = Number(price);
10332	if (!tax || isNaN(tax)) {
10333		return price;
10334	}
10335	tax = Number(tax);
10336	if (price !== 0 &&
10337			tax !== 0 &&
10338			$instance.current &&
10339			$instance.current.geo_buy &&
10340			$utils.isTrue($instance.current.geo_buy.show_prices_with_taxes)) {
10341		return (price * tax / 100) + price;
10342	}
10343	return price;
10344};
10345
10346$utils.getWithRetries = function(getter, retries, waitTime, attempt) {
10347	if (!retries) {
10348		retries = 5;
10349	}
10350	if (!waitTime) {
10351		waitTime = 300;
10352	}
10353	if (!attempt) {
10354		attempt = 0;
10355	}
10356	return new Promise(function(resolve) {
10357		if (attempt > retries) {
10358			resolve(null);
10359		} else {
10360			var result = getter();
10361			if (result === undefined) {
10362				setTimeout(function() {
10363					$utils.getWithRetries(getter, retries, waitTime, ++attempt).then(function(result) {
10364						resolve(result);
10365					})
10366				}, waitTime);
10367			} else {
10368				resolve(result);
10369			}
10370		}
10371	});
10372};
10373
10374$utils.prepareMapImages = function(entity, map) {
10375	if ($entity.isGeoLocation(entity) && $instance.hasEntity('mapimage')) {
10376		$rest.get({
10377			url: contextPath + "/api/geo/mapimage.json"
10378		}).then(function(response) {
10379			Promise.all(response.list.map(function(item) {
10380				return $rest.get({
10381					url: contextPath + "/api/geo/mapimage/" + item.id + ".json"
10382				});
10383			})).then(function(mapImages) {
10384				if (mapImages && mapImages.length > 0) {
10385					var overlays = [];
10386					mapImages.forEach(function(mapImage) {
10387						var bounds = new google.maps.LatLngBounds(
10388							new google.maps.LatLng(mapImage.south_west.latitude, mapImage.south_west.longitude),
10389							new google.maps.LatLng(mapImage.north_east.latitude, mapImage.north_east.longitude)
10390						);
10391						var image = contextPath + '/www/media/' + mapImage.map + '.io';
10392						var overlay = $utils.prepareMapImageOverlay(bounds, image);
10393
10394						$utils.addMapButton(map, $utils.parse(mapImage.name), function() {
10395							overlays.forEach(function(iter) {
10396								if (iter === overlay) {
10397									iter.show(map);
10398									map.fitBounds(bounds);
10399								} else {
10400									iter.hide();
10401								}
10402							});
10403						});
10404
10405						overlays.push(overlay);
10406					});
10407					$utils.addMapButton(map, $angular.get("$translate").instant('msg.normal'), function() {
10408						overlays.forEach(function(overlay) {
10409							overlay.hide();
10410						});
10411					});
10412				}
10413			});
10414		});
10415	}
10416};
10417
10418$utils.addMapButton = function(map, text, onClick) {
10419	var button = document.createElement('button');
10420	button.textContent = text;
10421	button.classList.add('btn', 'btn-primary');
10422	button.addEventListener('click', onClick);
10423	map.controls[google.maps.ControlPosition.TOP_RIGHT].push(button);
10424};
10425
10426$utils.prepareMapImageOverlay = function(bounds, image) {
10427	var overlayView = new google.maps.OverlayView();
10428	var div;
10429
10430	overlayView.onAdd = function() {
10431		div = document.createElement('div');
10432		div.style.borderStyle = 'none';
10433		div.style.borderWidth = '0px';
10434		div.style.position = 'absolute';
10435
10436		var img = document.createElement('img');
10437		img.src = image;
10438		img.style.width = '100%';
10439		img.style.height = '100%';
10440		img.style.position = 'absolute';
10441		div.appendChild(img);
10442
10443		var panes = overlayView.getPanes();
10444		panes.overlayLayer.appendChild(div);
10445	};
10446
10447	overlayView.draw = function() {
10448		var overlayProjection = overlayView.getProjection();
10449		var sw = overlayProjection.fromLatLngToDivPixel(
10450			bounds.getSouthWest()
10451		);
10452		var ne = overlayProjection.fromLatLngToDivPixel(
10453			bounds.getNorthEast()
10454		);
10455		if (div) {
10456			div.style.left = sw.x + 'px';
10457			div.style.top = ne.y + 'px';
10458			div.style.width = ne.x - sw.x + 'px';
10459			div.style.height = sw.y - ne.y + 'px';
10460		}
10461	};
10462
10463	overlayView.onRemove = function() {
10464		if (div) {
10465			div.parentNode.removeChild(div);
10466			div = null;
10467		}
10468	};
10469
10470	return {
10471		hide: function() {
10472			overlayView.setMap(null);
10473		},
10474		show: function(map) {
10475			overlayView.setMap(map);
10476		}
10477	};
10478};
10479
10480$utils.toggleItemOnList = function(obj, listName, item) {
10481	var list = obj[listName];
10482	if (!$utils.isArray(list)) {
10483		list = [];
10484		obj[listName] = list;
10485	}
10486	var index = list.indexOf(item);
10487	if (index < 0) {
10488		list.push(item);
10489	} else {
10490		list.splice(index, 1);
10491	}
10492};
10493
10494$utils.isEqual = function(object1, object2) {
10495	var keys1 = Object.keys(object1).filter(function(key) {
10496		return key !== '$$hashKey';
10497	});
10498	var keys2 = Object.keys(object2).filter(function(key) {
10499		return key !== '$$hashKey';
10500	});
10501	if (keys1.length !== keys2.length) {
10502		return false;
10503	}
10504	for (var key in object1) {
10505		if (key !== '$$hashKey' && object1[key] !== object2[key]) {
10506			return false;
10507		}
10508	}
10509	return true;
10510};
10511
10512$utils.clone = function(o) {
10513	if (o == null) {
10514		return null;
10515	}
10516	if ($utils.isArray(o)) {
10517		return o.map(function(prop) {
10518			return $utils.clone(prop);
10519		}).filter(function(prop) {
10520			return prop !== undefined && prop !== null;
10521		});
10522	}
10523	if (typeof o == 'object') {
10524		var result = {};
10525		var keys = Object.keys(o);
10526		for (var k in keys) {
10527			var key = keys[k];
10528			if (key == null || (typeof key == 'string' && key.startsWith('$'))) {
10529				continue;
10530			}
10531			var cloned = $utils.clone(o[key]);
10532			if (cloned !== undefined && cloned !== null) {
10533				result[key] = cloned;
10534			}
10535		}
10536		return result;
10537	}
10538	return o;
10539};
10540
10541$utils.openNextUploadFile = function(e) {
10542	$utils.preventDefault(e);
10543	$(e.target).next('input[type="file"]').trigger('click');
10544}
10545
10546$utils.addFileOnModel = function($scope, files, model, column, isMultiple) {
10547	if (files.length === 0) {
10548		return;
10549	}
10550	var file = files[0];
10551	var reader = new FileReader();
10552	reader.onload = function(event) {
10553		var base64 = file.name + ';' + event.target.result;
10554		if (isMultiple) {
10555			if (model[column]) {
10556				model[column].push(base64);
10557			} else {
10558				model[column] = [base64];
10559			}
10560		} else {
10561			model[column] = base64;
10562		}
10563		$angular.apply($scope);
10564	};
10565	reader.readAsDataURL(file);
10566}
10567
10568$utils.autoSave = function(model, column, pathPrefix) {
10569	if (!column._autosave) {
10570		return;
10571	}
10572
10573	var root = $angular.searchRootModel();
10574	if (root == null) {
10575		//$angular.buttonError(root, column, model, 'AutoSave: root not found');
10576		console.log('AutoSave: root not found');
10577		return;
10578	}
10579	
10580	var form = $angular.searchFormView();
10581	var formId; 
10582	if (form && form.form) {
10583		formId = form.form.id;
10584	}
10585
10586	var value = model[column.name];
10587	var path = column.path;
10588	if (path == null) {
10589		path = column.name;
10590	}
10591	if (path == null) {
10592		$angular.buttonError(root, column, model, 'AutoSave: path not found');
10593		return;
10594	}
10595	if (pathPrefix) {
10596		path = pathPrefix + path;
10597	}
10598	
10599	var entity = root.entity;
10600	var entityId = root.id;
10601	if (entity == null || entityId == null) {
10602		$angular.buttonError(root, column, model, 'AutoSave: entity/id not found');
10603		return;
10604	}
10605	
10606	$angular.get("$timeout")(function() {
10607		var baseUrl = contextPath + '/' + ($constants.isWeb ? 'www' : 'api');
10608		//console.log('saving ' + path + " = " + value);
10609		$rest.put({
10610			url: baseUrl + '/geo/' + entity + '/' + entityId + '/value.json',
10611			data: {
10612				path: path,
10613				formId: formId,
10614				value: value
10615			},
10616			loading: 'generic',
10617			successMessage: $angular.get("$translate").instant('msg.updated')
10618		}).then(function () {
10619			$angular.contentScope.refresh();
10620		});
10621	});
10622}
10623
10624$utils.prepareMediaColumnData = function(column) {
10625	var geoMediaFolder;
10626	var extension;
10627	var editFunction;
10628	var msgUploadKey;
10629	var acceptFileTypes;
10630	switch (column.type) {
10631		case 'image':
10632			geoMediaFolder = 'images';
10633			extension = 'img';
10634			editFunction = $angular.contentScope.editImage;
10635			msgUploadKey = 'msg.uploadImage';
10636			acceptFileTypes = 'image/*';
10637			break;
10638		case 'audio':
10639			geoMediaFolder = 'audio';
10640			extension = 'audio';
10641			editFunction = $angular.contentScope.editAudio;
10642			msgUploadKey = 'msg.uploadAudio';
10643			acceptFileTypes = 'audio/*';
10644			break;
10645		case 'video':
10646			geoMediaFolder = 'video';
10647			extension = 'video';
10648			editFunction = $angular.contentScope.editVideo;
10649			msgUploadKey = 'msg.uploadVideo';
10650			acceptFileTypes = 'video/*';
10651			break;
10652		case 'document':
10653			geoMediaFolder = 'others';
10654			extension = 'data';
10655			editFunction = $angular.contentScope.editOther;
10656			msgUploadKey = 'msg.uploadFile';
10657			acceptFileTypes = '*/*';
10658			break;
10659	}
10660	return {
10661		geoMediaFolder : geoMediaFolder,
10662		getLinkUrl : function(model, mediaId) {
10663			if (mediaId) {
10664				var base = $constants.isWeb ? 'www' : 'api';
10665				if (mediaId.extension) {
10666					return base + '/media/' + mediaId.id + '.' + mediaId.extension;
10667				}
10668				if (typeof mediaId === 'string') {
10669					if (extension) {
10670						return base + '/media/' + mediaId + '.' + extension;
10671					}
10672					if (model == null || model.geo_media == null) {
10673						model = $angular.contentScope.model; //$angular.searchRootModel(); 
10674					}
10675					if (model && model.geo_media && model.geo_media[geoMediaFolder] && model.geo_media[geoMediaFolder][mediaId]) {
10676						var media = model.geo_media[geoMediaFolder][mediaId];
10677						return base + '/media/' + mediaId + '.' + media.extension;
10678					}
10679					return base + '/media/' + mediaId + '.img';
10680				}
10681			}
10682			return null;
10683		},
10684		getFileName : function(model, mediaId) {
10685			if (mediaId) {
10686				if (mediaId.extension) {
10687					return mediaId.name + '.' + mediaId.extension;
10688				}				
10689				if (typeof mediaId === 'string') {
10690					if (model == null || model.geo_media == null) {
10691						model = $angular.contentScope.model; //$angular.searchRootModel(); 
10692					}
10693					if (model && model.geo_media && model.geo_media[geoMediaFolder] && model.geo_media[geoMediaFolder][mediaId]) {
10694						var media = model.geo_media[geoMediaFolder][mediaId];
10695						return media.name + '.' + media.extension;
10696					}
10697					return mediaId;
10698				}
10699			}
10700			return null;
10701		},
10702		editMedia : function($event, model, mediaId) {
10703			if (model == null || model.geo_media == null) {
10704				model = $angular.contentScope.model; //$angular.searchRootModel(); 
10705			}
10706			if (model && model.geo_media && model.geo_media[geoMediaFolder] && model.geo_media[geoMediaFolder][mediaId]) {
10707				var media = model.geo_media[geoMediaFolder][mediaId];
10708				editFunction($event, media);
10709			}
10710		},
10711		msgUploadKey: msgUploadKey,
10712		acceptFileTypes: acceptFileTypes
10713	};
10714}
10715
10716$utils.getColumnClasses = function(obj, column) {
10717	if ($utils.hideColumnByCondition(obj, column)) {
10718		if (column.required) {
10719			column._required = true;
10720			column.required = false;
10721		}
10722		return 'hidden';
10723	}
10724	if (column._required) {
10725		delete column._required;
10726		column.required = true;
10727	}
10728	if (column._classes) {
10729		return column._classes;
10730	}	
10731	return '';
10732};
10733
10734$utils.hideColumnByCondition = function(obj, column) {
10735	if (column.condition) {
10736		var value = false;
10737		try {
10738			value = eval(column.condition); // FIXME unsafe-eval
10739		} catch (e) {
10740			if ($constants.isDev) {
10741				console.log("Error eval condition: " + column.condition + " : " + e, e);
10742			}
10743		}
10744		
10745		if ($constants.isDev) {
10746			var value2 = false;
10747			try {
10748				value2 = evalExpression(obj, column.condition);
10749			} catch (e) {
10750				console.log("Error eval2 condition: " + column.condition + " : " + e, e);
10751			}
10752			if (value != value2) {
10753				console.log("Error compare evals ("+value+" vs "+value2+") condition: " + column.condition);
10754			}
10755		}
10756		
10757		return !value;
10758	}
10759	return false;
10760};
10761
10762function evalExpression(obj, expr) {
10763	const tokens = tokenize(expr);
10764	const rpn = toRPN(tokens); 
10765	return evalRPN(obj, rpn);
10766}
10767
10768function tokenize(expr) {
10769    const regex = /(===|!==|>=|<=|==|!=|&&|\|\||>|<|\(|\)|"(?:[^"\\]|\\.)*"|[a-zA-Z_][a-zA-Z0-9_.]*|\d+|true|false)/g;
10770    const tokens = expr.match(regex);
10771    return tokens ? tokens.map(t => t.trim()) : [];
10772}
10773
10774function toRPN(tokens) {
10775    const output = [];
10776    const stack = [];
10777
10778    for (const token of tokens) {
10779        if (token === "(") {
10780            stack.push(token);
10781        } else if (token === ")") {
10782            while (stack.length && stack[stack.length - 1] !== "(") {
10783                output.push(stack.pop());
10784            }
10785            stack.pop(); // remove "("
10786        } else if (precedence[token]) {
10787            while (
10788                stack.length &&
10789                precedence[stack[stack.length - 1]] >= precedence[token]
10790            ) {
10791                output.push(stack.pop());
10792            }
10793            stack.push(token);
10794        } else {
10795            output.push(token);
10796        }
10797    }
10798
10799    while (stack.length) {
10800        output.push(stack.pop());
10801    }
10802
10803    return output;
10804}
10805
10806const precedence = {
10807    "||": 1,
10808    "&&": 2,
10809    "==": 3, 
10810	"!=": 3, 
10811	"===": 3, 
10812	"!==": 3,
10813    ">": 4, 
10814	"<": 4, 
10815	">=": 4, 
10816	"<=": 4
10817};
10818
10819function evalRPN(obj, rpn) {
10820    const stack = [];
10821
10822    for (const token of rpn) {
10823        if (ops[token]) {
10824            const b = stack.pop();
10825            const a = stack.pop();
10826            stack.push(ops[token](a, b));
10827        } else {
10828            // operand
10829            if (token.startsWith("obj.")) {
10830                stack.push(getDeep(obj, token.substring(4)));
10831            } else if (/^[a-zA-Z_]/.test(token)) {
10832                stack.push(getDeep(obj, token));
10833            } else {
10834                stack.push(parseValue(token));
10835            }
10836        }
10837    }
10838
10839    return stack.pop();
10840}
10841
10842const ops = {
10843    "===": (a, b) => a === b,
10844    "!==": (a, b) => a !== b,
10845    "==":  (a, b) => a == b,
10846    "!=":  (a, b) => a != b,
10847    ">":   (a, b) => a > b,
10848    "<":   (a, b) => a < b,
10849    ">=":  (a, b) => a >= b,
10850    "<=":  (a, b) => a <= b,
10851    "&&":  (a, b) => a && b,
10852    "||":  (a, b) => a || b
10853};
10854
10855function getDeep(obj, path) {
10856    return path.split('.').reduce((o, k) => o?.[k], obj);
10857}
10858
10859function parseValue(raw) {
10860    try {
10861        return JSON.parse(raw);
10862    } catch {
10863        return raw.replace(/^"|"$/g, "");
10864    }
10865}
10866
10867
10868
10869//$utils.setModel = function(m, o) {
10870//	if (m == null) {
10871//		return o;
10872//	}
10873//	
10874//	// clean instance
10875//	for (var k in m) {
10876//		if ($utils.isObject(m[k]) && !$utils.isArray(m[k]) && !k.startsWith("geo_")) {
10877//			m[k] = $utils.setModel(m[k], {});		
10878//		} else {
10879//			m[k] = null; // delete no work
10880//		}
10881//	}
10882//	
10883//	// copy data
10884//	for (var k in o) {
10885//		if ($utils.isObject(o[k]) && !$utils.isArray(o[k])) {
10886//			m[k] = $utils.setModel(m[k], o[k]);
10887//		} else {
10888//			m[k] = o[k];
10889//		}
10890//	}
10891//	return m;
10892//};
10893
10894$utils.setFormviews = function(m, o) {
10895	if (o._formviews == null) o._formviews = [];
10896	if (m == null) {
10897		m = o._formviews;
10898	} else {
10899		// update only data
10900		for (var k in m) {
10901			var fv = m[k];
10902			var fvo = o._formviews[k];
10903			fv.data = fvo.data;//$utils.setModel(fv.data, fvo.data);
10904		}
10905	}
10906	delete o._formviews;
10907	return m;	
10908};
10909
10910$utils.printBoolean = function(value, isHtml) {
10911	var isTrue = $utils.isTrue(value);
10912	if (isHtml) {
10913		if (isTrue) {
10914			return '<span class="material-icons">done</span>';
10915		}
10916		return '<span class="material-icons">clear</span>';
10917	}
10918	if (isTrue) {
10919		return "\u2714";
10920	}
10921	return "";//"\u2717";
10922};
10923
10924$utils.tryToTranslate = function($translate, keys, params) {
10925	for (var iter = 0; iter < keys.length; iter++) {
10926		var key = keys[iter];
10927		var text = $translate.instant(key, params);
10928		if (text !== key) {
10929			return text;
10930		}
10931	}
10932	return null;
10933};
10934
10935$utils.goToGeoObject = function(entity, id, preserveQueryString) {
10936	var href = $angular.rootScope.getItemLink(entity, id);
10937	if (preserveQueryString) {
10938		href += window.location.search;
10939	}
10940	window.location.href = href;
10941};
10942
10943$utils.printHtml = function (html, test) {
10944	if (test) {
10945		window.open().document.write(html);
10946		return;
10947	}
10948    var hiddenFrame = $('<iframe style="visibility: hidden"></iframe>').appendTo('body')[0];
10949    hiddenFrame.contentWindow.printAndRemove = function () {
10950        hiddenFrame.contentWindow.print();
10951		setTimeout(function() {
10952	        $(hiddenFrame).remove();
10953		}, 3000);
10954    };
10955    var htmlContent = "<html><body onload='printAndRemove();'>" + html + "</body></html>";
10956    var doc = hiddenFrame.contentWindow.document.open("text/html", "replace");
10957    doc.write(htmlContent);
10958    doc.close();
10959};
10960
10961$utils.printUrl = function (url, num) {
10962	console.log(url);
10963
10964	$utils.showLoading();
10965//    setTimeout(function() {
10966//    }, 3000);
10967		
10968   	var hiddenFrame = $('<iframe style="visibility: hidden"></iframe>').appendTo('body')[0];
10969	hiddenFrame.contentWindow.printAndRemove = async () => {
10970        const canvas = hiddenFrame.contentDocument.getElementById('pdf-canvas');
10971        const ctx = canvas.getContext('2d');
10972        const pdf = await hiddenFrame.contentWindow.pdfjsLib.getDocument(url).promise;
10973        const page = await pdf.getPage(1); // Obtén la primera página del PDF
10974        const viewport = page.getViewport({ scale: 1 });
10975        canvas.width = viewport.width;
10976        canvas.height = viewport.height;
10977        const renderContext = {
10978            canvasContext: ctx,
10979            viewport: viewport
10980        };
10981        await page.render(renderContext).promise;
10982        $utils.hideLoading();
10983        hiddenFrame.contentWindow.print();
10984        $utils.hideLoading();
10985        
10986    	//for (var i=0; i<num; i++) {	}
10987        if (num > 1) {
10988        	num--;
10989        	$rest.openConfirmPanel("msg.print.next", "", function() {
10990	        	$utils.printUrl(url, num);
10991			}, function() {});
10992        }
10993    };
10994
10995    var htmlContent = 
10996    	  "<html>"
10997    	+ "<head>"
10998    	+ "<style>@media print { @page { margin: 0; } body { margin: 0; } ::-webkit-scrollbar { display: none; } }</style>"
10999    	+ "<script src='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js'></script>"
11000    	+ "</head>"
11001    	+ "<body onload='printAndRemove();'>
11001<canvas id='pdf-canvas'></canvas></body>"
11002    	+ "</html>";
11003    	
11004    var doc = hiddenFrame.contentWindow.document.open("text/html", "replace");
11005    doc.write(htmlContent);
11006    doc.close();
11007	
11008};
11009
11010$utils.contains = function(array, item) {
11011	return $utils.isArray(array) && array.indexOf(item) > -1;
11012};
11013
11014$utils.validateEnabledObject = function(entity, id) {
11015	var defer = $angular.get('$q').defer();
11016	if (id) {
11017		$constants.getDataMin(entity, id, function(obj) {
11018			if (!obj) {
11019				defer.reject('notFound');
11020			} else if (!obj.enabled) {
11021				defer.reject('disabled');
11022			} else {
11023				defer.resolve(true);
11024			}
11025		});
11026	} else {
11027		defer.reject('empty');
11028	}
11029	return defer.promise;
11030};
11031
11032$utils.pushDistinct = function(obj, attrName, item) {
11033	if (obj && attrName && item) {
11034		var list = obj[attrName];
11035		if (!list) {
11036			list = [];
11037			obj[attrName] = list;
11038		}
11039		if ($utils.isArray(list)) {
11040			if ($utils.isObject(item)) {
11041				var contains = list.find(function(i) {
11042					return $utils.isEqual(item, i);
11043				});
11044				if (!contains) {
11045					list.push($utils.clone(item));
11046				}
11047			} else if (!list.includes(item)) {
11048				list.push(item);
11049			}
11050		}
11051	}
11052};
11053
11054$utils.isFormula = function(text) {
11055	return true;
11056//	if (!text) {
11057//		return false;
11058//	}
11059//	return /^[a-z\d_]+\(.*\)$/g.test(text.trim());
11060};
11061
11062$utils.array_move = function(arr, old_index, new_index) {
11063    if (new_index >= arr.length) {
11064        var k = new_index - arr.length + 1;
11065        while (k--) {
11066            arr.push(undefined);
11067        }
11068    }
11069    arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
11070};
11071
11072$utils.isGeoObjectId = function(id) {
11073	return id && /^[0-9a-f]{24}$/.test(id);
11074};
11075
11076$utils.deepEquals = function(o1, o2) {
11077	if ($utils.isArray(o1) && $utils.isArray(o2)) {
11078		if (o1.length !== o2.length) {
11079			return false;
11080		}
11081		for (var iter = 0; iter < o1.length; iter++) {
11082			if (!$utils.deepEquals(o1[iter], o2[iter])) {
11083				return false;
11084			}
11085		}
11086		return true;
11087	} else if ($utils.isObject(o1) && $utils.isObject(o2)) {
11088		var keys1 = Object.keys(o1);
11089		var keys2 = Object.keys(o2);
11090		if (keys1.length !== keys2.length) {
11091			return false;
11092		}
11093		for (var iterKey = 0; iterKey < keys1.length; iterKey++) {
11094			var key = keys1[iterKey];
11095			if (keys2.indexOf(key) < 0) {
11096				return false;
11097			}
11098			if (!$utils.deepEquals(o1[key], o2[key])) {
11099				return false;
11100			}
11101		}
11102		return true;
11103	} else {
11104		return o1 === o2;
11105	}
11106};
11107
11108//// File entity.js ////
11109var $entity = {};
11110
11111$entity.isAppObject = function(entity) {
11112	var meta = $entity.getEntity(entity);
11113	return meta != null && meta.type === "entity";
11114};
11115
11116$entity.isCustomId = function(entity) {
11117	var meta = $entity.getEntity(entity);
11118	return meta != null && $utils.isTrue(meta.custom_id);
11119};
11120
11121$entity.isCreateSimple = function(entity) {
11122	var meta = $entity.getEntity(entity);
11123	return meta && meta.options && $utils.isTrue(meta.options.create_simple);
11124};
11125$entity.isAttachDocuments = function(entity) {
11126	var meta = $entity.getEntity(entity);
11127	return meta && meta.options && $utils.isTrue(meta.options.attach_documents);
11128};
11129$entity.isDisplayTable = function(entity) {
11130	var meta = $entity.getEntity(entity);
11131	return meta && meta.options && $utils.isTrue(meta.options.display_table) && !$utils.isScreenSmall;
11132};
11133$entity.isDisplayTableWeb = function(entity) {
11134	var meta = $entity.getEntity(entity);
11135	return meta && meta.options && $utils.isTrue(meta.options.display_table_web);
11136};
11137$entity.isHideMedia = function(entity) {
11138	var meta = $entity.getEntity(entity);
11139	return meta && meta.options && $utils.isTrue(meta.options.hide_media);
11140};
11141$entity.isShowProperties = function(entity) {
11142	var meta = $entity.getEntity(entity);
11143	return meta && meta.options && $utils.isTrue(meta.options.show_properties);
11144};
11145$entity.isShowKeyValues = function(entity) {
11146	var meta = $entity.getEntity(entity);
11147	return meta && meta.options && $utils.isTrue(meta.options.show_key_values);
11148};
11149$entity.isReadonly = function(entity) {
11150	var meta = $entity.getEntity(entity);
11151	return meta && meta.options && $utils.isTrue(meta.options.readonly);
11152};
11153$entity.isOneItem = function(entity) {
11154	var meta = $entity.getEntity(entity);
11155	return meta && meta.options && $utils.isTrue(meta.options.one_item);
11156};
11157$entity.isAutoLocation = function(entity) {
11158	var meta = $entity.getEntity(entity);
11159	return meta && meta.options && $utils.isTrue(meta.options.auto_location);
11160};
11161$entity.isModeler = function(entity) {
11162	var meta = $entity.getEntity(entity);
11163	return meta && meta.options && $utils.isTrue(meta.options.modeler);
11164};
11165
11166$entity.isTabVisible = function(entity, value) {
11167	var meta = $entity.getEntity(entity);
11168	var hidden = meta && meta.tabs && $utils.isTrue(meta.tabs[value]);
11169	return !hidden;
11170};
11171
11172$entity.getCalendarColumns = function(entity) {
11173	var getColumn = function(by) {
11174		if (by === 'sys.created' || by === 'sys.updated') {
11175			return { name: by, type: 'datetime'	};
11176		}
11177		return $entity.getColumn(entity, by);
11178	};
11179	var dateColumn;
11180	var timeColumn;
11181	var meta = $entity.getEntity(entity);
11182	var calendarDate = meta ? meta.calendar_date : null;
11183	var isGeoSchedule = $entity.isGeoSchedule(entity);
11184	if (calendarDate === 'geo_schedule') {
11185		isGeoSchedule = true;
11186	} else if (calendarDate) {
11187		isGeoSchedule = false;
11188		var parts = calendarDate.split(',');
11189		for (var i = 0; i < parts.length; i++) {
11190			var column = getColumn(parts[i]);
11191			var type = $entity.getColumnType(column);
11192			if (type === 'datetime') {
11193				return { date: column };
11194			}
11195//			if (type === 'formula') {
11196//				return { date: column };
11197//			}
11198			if (type === 'date' && !dateColumn) {
11199				dateColumn = column;
11200			}
11201			if (type === 'time' && !timeColumn) {
11202				timeColumn = column;
11203			}
11204			if (dateColumn && timeColumn) {
11205				return { date: dateColumn, time: timeColumn };
11206			}
11207		}
11208	}
11209	return { date: dateColumn ? dateColumn : getColumn('sys.created'), geoSchedule: isGeoSchedule };
11210};
11211
11212$entity.hasBinding = function(value, entity) {
11213	var meta = $entity.getEntity(entity);
11214	return meta != null && meta.binding != null && meta.binding.indexOf(value) !== -1;
11215};
11216
11217$entity.isGeoUser = function(entity) {
11218	return $entity.hasBinding("geo_user", entity);
11219};
11220$entity.isGeoLocation = function(entity) {
11221	return $entity.hasBinding("geo_location", entity) || $entity.hasBinding("geo_fence", entity);
11222};
11223$entity.isGeoSchedule = function(entity) {
11224	var meta = $entity.getEntity(entity);
11225	if (meta && meta.id === 'instance') {
11226		return $instance.hasGeoSchedule();
11227	}
11228	return $entity.hasBinding("geo_schedule", entity);
11229};
11230$entity.isGeoTrivia = function(entity) {
11231	return $entity.hasBinding("geo_trivia", entity);
11232};
11233$entity.isGeoPrize = function(entity) {
11234	return $entity.hasBinding("geo_prize", entity);
11235};
11236$entity.isGeoForm = function(entity) {
11237	return $entity.hasBinding("geo_form", entity);
11238};
11239$entity.isGeoBuy = function(entity) {
11240	var meta = $entity.getEntity(entity);
11241	if (meta && meta.id === 'instance') {
11242		return $instance.hasGeoBuy();
11243	}
11244	return $entity.hasBinding("geo_buy", entity);
11245};
11246$entity.isGeoSmartRate = function(entity) {
11247	if ($instance.current && $instance.current.geo_smarter) {
11248		var meta = $entity.getEntity(entity);
11249		var smarter = $instance.current.geo_smarter;
11250		if (meta && smarter.entities && !$utils.isTrue(smarter.disabled)) {
11251			return smarter.entities.indexOf(meta.id) !== -1;
11252		}
11253	}
11254	return false;
11255};
11256$entity.isGeoCalendar = function(entity) {
11257	return $entity.hasBinding("geo_calendar", entity);
11258};
11259
11260$entity.getColumnsTranslations = function(entity, model) {
11261	var columns = [];
11262	var meta = $entity.getEntity(entity);
11263	if (meta) {
11264		$entity.addColumnsTextTranslations(columns, meta.columns, meta, model);
11265		
11266		if ($entity.isGeoLocation(entity)) {
11267			columns.push({name: 'address', type: 'geo_location'});
11268			columns.push({name: 'hint', type: 'geo_location'});
11269		}
11270		if ($entity.isGeoTrivia(entity)) {
11271			columns.push({name: 'question', type: 'geo_trivia'});
11272			columns.push({name: 'answer_explanation', type: 'geo_trivia'});
11273		}
11274		if ($entity.isGeoPrize(entity)) {
11275			columns.push({name: 'hint', type: 'geo_prize'});
11276		}
11277		if (meta.id === 'instance') {
11278			columns.push({name: 'title', type: 'geo_login'});
11279		}
11280	}
11281	return columns;
11282};
11283
11284$entity.addColumnsTextTranslations = function(translations, columns, meta, model, type) {
11285	if (columns) {
11286		columns.forEach(function (col) {
11287			if (col.type === 'text') {
11288				translations.push({
11289					name: col.name,
11290					html: col.html,
11291					type: type ? type : 'text',
11292					key: type ? type + '.' + col.name : undefined,
11293					getValue: function() {
11294						return $utils.get(model, type.replaceAll('[', '.').replaceAll(']', '') + '.' + col.name);
11295					}
11296				});
11297			} else if (col.type === 'inner_entity') {
11298				if (meta && meta.inner_entities) {
11299					var inner = meta.inner_entities.filter(function(it) {
11300						return it.name === col.inner_entity;
11301					})[0] || {};
11302					$entity.addColumnsTextTranslations(translations, inner.columns, meta, model, (type ? type + '.'  : '') + col.name);
11303				}
11304			} else if (col.type === 'inner_entity_list') {
11305				var path = (type ? type + '.'  : '') + col.name;
11306				var data = $utils.get(model, path);
11307				if (meta && meta.inner_entities && data && data.length > 0) {
11308					var innerColumns = (meta.inner_entities.filter(function(it) {
11309						return it.name === col.inner_entity;
11310					})[0] || {}).columns;
11311					for (var iter = 0; iter < data.length; iter++) {
11312						$entity.addColumnsTextTranslations(translations, innerColumns, meta, model, path + '[' + iter + ']');
11313					}
11314				}
11315			}
11316		});
11317	}
11318}
11319
11320$entity.isItemValid = function(entity, item) {
11321	return !$entity.isGeoSchedule(entity) || $utils.hasValidScheduledDates(item);
11322};
11323
11324//inner entities 
11325$entity.getColumnsInner = function(entity, column) {
11326	var meta = $entity.getEntity(entity);
11327	if (meta == null) {
11328		return [];
11329	}
11330	if (meta.columnsInnerEntity == null) {
11331		meta.columnsInnerEntity = {};
11332	}
11333	if (meta.columnsInnerEntity[column.name] == null) {
11334		if (meta.inner_entities) {
11335			for (var k in meta.inner_entities) {
11336				var inner = meta.inner_entities[k];
11337				if (inner.name === column.inner_entity) {
11338					meta.columnsInnerEntity[column.name] = inner.columns;
11339					break;
11340				}
11341			}
11342		}
11343	}
11344	return meta.columnsInnerEntity[column.name];
11345};
11346
11347$entity.getColumnData = function(model, column) {
11348	if (model[column.name] == null && column.type === 'inner_entity') {
11349		model[column.name] = {};
11350	}
11351	return model[column.name];
11352};
11353$entity.addColumnItem = function(model, column, withId, data) {
11354	if (!$utils.isArray(model[column])) {
11355		model[column] = [];
11356	}
11357	var item = data ? data : {};
11358	if (withId && !item.id) {
11359		item.id = '' + Date.now();
11360	}
11361	model[column].push(item);
11362	return item;
11363};
11364$entity.removeColumnItem = function(model, column, index) {
11365	if (!$utils.isArray(model[column])) {
11366		model[column] = [];
11367	} else {
11368		model[column].splice(index, 1);
11369	}
11370};
11371
11372$entity.displayColumnData = function(entity, id, column) {
11373	if (id == null) {
11374		return null;
11375	}
11376	if (typeof id != 'string') {
11377		return $entity.displayColumn(id, column);
11378	}
11379	if (column && column.default) {
11380		var value = $constants.names[id];
11381		if (value) {
11382			return value;
11383		}
11384	}
11385	//console.log("getting " + entity + " " + id)
11386	var data = $constants.getDataMin(entity, id);
11387	return $entity.displayColumn(data, column);
11388}
11389
11390$entity.displayColumnHtml = function(model, column, fullHtml) {
11391	return $entity.displayColumn(model, column, true, fullHtml);
11392};
11393
11394$entity.displayColumnTable = function(o) {
11395	if (o == null) {
11396		return null;
11397	}
11398	if (o.entity) {
11399		return $entity.displayColumn(o);
11400	}
11401	if (o.name) {
11402		return $utils.parseGeoTranslation(o.name);
11403	}
11404	return null;
11405};
11406
11407$entity.displayColumn = function(model, column, isHtml, fullHtml) {
11408	
11409	if (model == null) {
11410		return null;
11411	}
11412	if (column == null) {
11413		if (model._meta) { // for trash
11414			column = $entity.getDefaultColumn(model._meta);
11415		}
11416		if (column == null && model.entity && model.entity != 'entity') {
11417			column = $entity.getDefaultColumn(model.entity);
11418		}
11419		if ($constants.isWeb && column != null && $utils.isTrue(column.hidden)) {
11420			return "";
11421		}
11422	}
11423	if (isHtml == null) isHtml = false;
11424	if (!isHtml || fullHtml == null) fullHtml = false;
11425
11426	var value = $entity._displayColumn(model, column, fullHtml);
11427	
11428	if (!fullHtml && (column == null || column.default) && value && model.id) {
11429		$constants.names[model.id] = value;
11430	}
11431	
11432	if (typeof value == 'string') {
11433		var colHtml = column && column.html;
11434		if (!colHtml && isHtml) {
11435			value = value.replace(/\n/g, "<br>");
11436		} else if (colHtml && !isHtml) {
11437			value = $utils.cleanHtml(value);
11438		}
11439//		if (!isHtml && value.length > 30) {
11440//			value = value.substring(0,29)+"...";
11441//		}
11442	}
11443	
11444	return value;
11445};
11446
11447$entity._displayColumn = function(model, column, isHtml) {
11448	
11449	if (column == null) {
11450		if (model.id && $constants.names[model.id]) {
11451			return $constants.names[model.id];
11452		}
11453		if ($constants.isWeb && model.name_web) {
11454			return $utils.parse(model.name_web);
11455		}
11456		if (model.name) { // for entity and others
11457			return $utils.parse(model.name); 
11458		}
11459		return null;
11460	}	
11461	
11462//	if ($constants.isWeb && $utils.isTrue(column.hidden)) { //  TODO if is default column is a problem
11463//		return "";
11464//	}
11465	
11466	if (column.default && model.id && $constants.names[model.id] && !isHtml) {
11467		return $constants.names[model.id];
11468	}
11469	
11470	if (column.name == 'categories' || column.type == 'categories') {
11471		return $constants.getCategoryListDesc(model[column.name]);
11472	}
11473
11474	var type = column.type;
11475	if (type === 'formula' && column.formula_type) {
11476		type = column.formula_type;
11477	}
11478
11479	var o = null;
11480	if (column.name) {
11481		o = model[column.name];
11482	}
11483	if (o == null) {
11484		if (type === 'boolean' || type === 'boolean_int') {
11485			o = false;
11486		} else {
11487			return "";
11488		}
11489	}
11490	//console.log(column.name, type, o)
11491	
11492	if (type === 'aggregation') {
11493		if ($utils.isArray(o) && o.length > 0) {
11494
11495			var f = column.aggregation_formula ? column.aggregation_formula : column.formula;
11496			if (f == null) {
11497				f = "";
11498			}
11499			f = f.trim().split("(")[1].split(")")[0];
11500			var split = f.split(",");
11501			var e = split[0].trim();
11502			var g = split[1].trim();
11503			var col = $entity.getColumn(e, g);
11504			var query = null;
11505			if (split.length > 3) {
11506				query = "";
11507				for (var i=3; i<split.length; i++) {
11508					query += "&" + split[i].trim();
11509				}
11510				//console.log("query", query)
11511			}
11512			
11513			var list = [];
11514			for (var key in o) {
11515				var item = o[key];
11516				item = $utils.clone(item);
11517				
11518				if (item.id == null) {
11519					item.id = "-";
11520				} else {
11521					if ($utils.isArray(item.id) && item.id.length > 0) {
11522						item.id = item.id[0];
11523					}
11524					if (col && col.param) {
11525						item.id = $entity.displayColumnData(col.param, item.id);
11526					} else {
11527						//item.id = $utils.toText(item.id); TODO comented to keep original text
11528					}
11529				}
11530								
11531				for (var k in item) {
11532					var v = item[k];
11533					if (v != null && k.contains("_")) {
11534						var cn = k.split("_")[1];
11535						var co = $entity.getColumn(e, cn);
11536						if (co && (co.type === 'money' || co.formula_type === 'money')) {
11537							item[k] = $utils.printCurrency(v, co.currency);
11538							continue;
11539						}
11540						if (co && (co.type === 'percentage' || co.formula_type === 'percentage')) {
11541							item[k] = $utils.parseNumber(Number(v).toFixed(2)) + '%';
11542							continue;
11543						}
11544					}
11545					item[k] = $utils.parse(v);
11546				}
11547				list.push(item);
11548			}
11549			
11550			// sort by id
11551			list.sort(function(a, b) {
11552				var y = a.id;
11553				var x = b.id;
11554				if (x > y) {
11555					return -1;
11556				}
11557				if (y > x) {
11558					return 1;
11559				}
11560				return 0;
11561			});
11562
11563			if (isHtml) {
11564				var result = "";
11565				result += "<ul class='list-group'>";
11566				
11567				for (var key in list) {
11568					var item = list[key];
11569					var isOneValue = $utils.keys(item).length < 3;
11570					
11571					var li = "<div class='row'>";
11572					for (var k in item) {
11573						var v = item[k];
11574						if (k == 'id' && v == '-') {
11575							continue;
11576						}
11577						
11578						if (isOneValue) {
11579							li += "<div class='col-12'>";
11580						} else {
11581							li += "<div class='col-sm col-6'>";
11582						}
11583						
11584						if (k == 'id') {
11585							li += "<small>"+v+"</small>";
11586						} else if (isOneValue) {
11587							li += "<label>"+v+"</label>";
11588						} else {
11589							li += "<small>"+k+"</small><br><label>"+v+"</label>";
11590						}
11591						li += "</div>";
11592					}
11593					li += "</div>";
11594					
11595					if (e && g) {
11596						var link;
11597						if (item.id != '-') {
11598							link = $instance.getBaseUrl() + e + "?" + g + "=" + item.id;
11599						} else {
11600							link = $instance.getBaseUrl() + e + "?" + g + ":$exists=false";
11601						}
11602						link += $entity.parseQuery(query, model);
11603						//console.log("link", link)
11604						li = "<a href='"+link+"'>" + li + "</a>";
11605					}
11606					result += "<li class='list-group-item table-relation'>" + li + "</li>";
11607				}
11608				result += "</ul>";
11609				return result;
11610			}
11611
11612			var result = [];
11613			for (var key in list) {
11614				var item = list[key];
11615				
11616				var txt = "";
11617				var isOneValue = $utils.keys(item).length < 3;
11618				for (var k in item) {
11619					var v = item[k];
11620					if (k == 'id' && v == '-') {
11621						continue;
11622					}
11623					if (txt.length > 0) {
11624						txt += ": ";
11625					}
11626					if (k == 'id' || isOneValue) {
11627						txt += v;
11628					} else {
11629						txt += k+":"+v + " ";
11630					}
11631				}
11632				result.push(txt);
11633			}
11634			return $utils.join(result, ", ");
11635		}
11636	}
11637
11638	if (type === 'datetime') {
11639		return $utils.parseDateTotal(o);
11640	}
11641	if (type === 'date') {
11642		return $utils.parseDay(o);
11643	}
11644	if (type === 'money') {
11645		return $utils.printCurrency(o, column.currency);
11646	}
11647	if (type === 'percentage') {
11648		return $utils.parseNumber(Number(o).toFixed(2)) + '%';
11649	}
11650	if (type === 'boolean' || type === 'boolean_int') {
11651		if (Array.isArray(o)) {
11652			return o.reduce(function (a, b) {
11653				return a + $utils.printBoolean(b, isHtml);
11654			}, '');
11655		}
11656		return $utils.printBoolean(o, isHtml);
11657	}
11658	if (type === 'entity') {
11659		var e = column.param;
11660		if (o && o.entity) {
11661			e = o.entity;
11662		}
11663		var entityDesc = $constants.getEntityDesc(e, o);
11664		if (entityDesc && isHtml && !$constants.isWeb && e) {
11665			var url = $angular.rootScope.getItemLink(e, o);
11666			return '<a href="' + url + '">' + entityDesc + '</a>';
11667		}
11668		return entityDesc;
11669	}
11670	if (type === 'any_entity') {
11671		if ($utils.isArray(o)) {
11672			type = "any_entity_list";
11673		} else {
11674			if (o.entity && o.entity_id) {
11675				var entityDesc = $constants.getEntityDesc(o.entity, o.entity_id);
11676				if (entityDesc && isHtml && !$constants.isWeb) {
11677					var url = $angular.rootScope.getItemLink(o.entity, o.entity_id);
11678					return '<a href="' + url + '">' + entityDesc + '</a>';
11679				}
11680				return entityDesc;
11681			}
11682			return null;
11683		}
11684	}
11685	if (type === 'any_entity_list') {
11686		
11687		if (isHtml && !$constants.isWeb) {
11688			var ul = "<ul class='list-group'>";
11689			for (var k in o) {
11690				var item = o[k];
11691				if ($utils.isArray(item) && item.length > 0) {
11692					item = item[0];
11693				}
11694				var text = $constants.getEntityDesc(item.entity, item.entity_id);
11695				var url = $angular.rootScope.getItemLink(item.entity, item.entity_id);
11696				ul += "<li class='list-group-item table-relation'>";
11697				ul += "<a class='row' href='" + url + "'>
11697" + text + "</a></li>";
11698			}
11699			ul += "</ul>";
11700			return ul;
11701			//return $utils.parseToHtml(o, column.name);
11702		}
11703		
11704		var result = [];
11705		for (var k in o) {
11706			var item = o[k];
11707			result.push($constants.getEntityDesc(item.entity, item.entity_id));
11708		}
11709		return $utils.join(result, ", ");
11710	}
11711	if (type === 'list') {
11712
11713		var e = column.param;
11714		if (o && o.entity) {
11715			e = o.entity;
11716		}
11717
11718		if (isHtml && !$constants.isWeb) {
11719			var ul = "<ul class='list-group'>";
11720			for (var k in o) {
11721				var item = o[k];
11722				if ($utils.isArray(item) && item.length > 0) {
11723					item = item[0];
11724				}
11725				var text, url;
11726				if (typeof item == 'string') {
11727					text = $constants.getEntityDesc(e, item);
11728					url = $angular.rootScope.getItemLink(e, item);
11729				} else {
11730					text = $entity.displayColumn(item);
11731					url = $angular.rootScope.getItemLinkBy(item);
11732				}
11733				ul += "<li class='list-group-item table-relation'>";
11734				ul += "<a class='row' href='" + url + "'>" + text + "</a></li>";	
11735			}
11736			ul += "</ul>";
11737			return ul;
11738			//return $utils.parseToHtml(o, column.name);
11739		}
11740		
11741		var result = [];
11742		for (var k in o) {
11743			var item = o[k];
11744			if ($utils.isArray(item) && item.length > 0) {
11745				item = item[0];
11746			}
11747			if (typeof item == 'string') {
11748				result.push($constants.getEntityDesc(e, item));
11749			} else {
11750				result.push($entity.displayColumn(item));
11751			}
11752		}
11753		if (result.some(function(val) {
11754			return val === undefined;
11755		})) {
11756			return undefined;
11757		}
11758		return $utils.join(result, ", ");
11759	}
11760	if (type === 'select' || type === 'enum' || type === 'enum_list') {
11761		if (Array.isArray(o)) {
11762			o = o.map(function(item) {
11763				return $utils.parseGeoTranslation(item, null, null, 'geo_translations.' + column.name + '$options');
11764			});
11765		} else {
11766			o = $utils.parseGeoTranslation(o, null, null, 'geo_translations.' + column.name + '$options');
11767		}
11768	}
11769	if (type === 'json') {
11770		try {
11771			o = angular.toJson(angular.fromJson(o), 2); // pretty print
11772		} catch (e) {}
11773		if (isHtml) {
11774			o = '<div class="code-readonly"><pre><code>' + o + '</code></pre></div>';
11775		}
11776		return o;
11777	}
11778	if (type === 'image' || type === 'audio' || type === 'video' || type === 'document') {
11779		var isMultiple = !!column.multiple;
11780		if (!Array.isArray(o)) {
11781			o = [o];
11782		}
11783		
11784		var mediaTmp = $utils.prepareMediaColumnData(column);
11785		var result = o.map(function(mediaId) {
11786			var text = mediaTmp.getFileName(model, mediaId);
11787			if (isHtml) {
11788				text = '<a target="_blank" href="' + mediaTmp.getLinkUrl(model, mediaId) + '">
11788' + text + '</a>';
11789				if (isMultiple) {
11790					text = '<li>' + text + '</li>';
11791				}
11792			}
11793			return text;
11794		}).join(isHtml ? '' : ',');
11795		
11796		if (isHtml && isMultiple) {
11797			result = '<ul class="list-unstyled">' + result + '</ul>';
11798		}
11799		return result;
11800	}
11801	if (type === 'text') {
11802		o = $utils.parse(o);
11803	}
11804	if (isHtml) {
11805		o = $utils.parseToHtml(o);
11806	}
11807	o = $utils.parse(o);
11808	//console.log(column.name + " -> " + o + " - " + (typeof o))
11809	return o;
11810};
11811
11812$entity.getColumnByPath = function(entity, innerEntity, path, prePath) {
11813	var meta;
11814	if (innerEntity) {
11815		meta = $entity.getInnerEntity(entity, innerEntity);
11816	} else {
11817		meta = $entity.getEntity(entity);
11818	}
11819	var parts = path;
11820	if (typeof path === 'string') {
11821		parts = path.split('.');
11822	}
11823	var colName = parts[0];
11824	var column = meta.columns.find(function(col) {
11825		return col.name === colName;
11826	});
11827	if (!column) {
11828		return null;
11829	}
11830	var pathToColumn = (prePath ? prePath + "." : "") + colName;
11831	parts.splice(0, 1);
11832	if (parts.length === 0) {
11833		return {
11834			entity: entity,
11835			innerEntity: innerEntity,
11836			column: column,
11837			pathToColumn: pathToColumn
11838		};
11839	}
11840	var type = column.type;
11841	if ((type === 'entity' || type === 'list' || type === 'relation_entity' || type === 'relation') && column.param) {
11842		return $entity.getColumnByPath(column.param, null, parts);
11843	}
11844	if ((type === 'inner_entity' || type === 'inner_entity_list') && column.inner_entity) {
11845		return $entity.getColumnByPath(entity, column.inner_entity, parts, pathToColumn);
11846	}
11847	if (type === 'formula') {
11848		var formula = column.formula;
11849		if (formula != null && formula.trim().startsWith("relation")) {
11850			var relationEntity = formula.substring(formula.indexOf("(")+1);
11851			relationEntity = relationEntity.substring(0, relationEntity.indexOf(",")).trim();
11852			return $entity.getColumnByPath(relationEntity, null, parts);
11853		}
11854	}
11855	return null;
11856};
11857
11858$entity.hasColumn = function(entity, path) {
11859	return $entity.getColumn(entity, path) != null;
11860};
11861
11862$entity.getColumn = function(entity, path) {
11863	var meta = $entity.getEntity(entity);
11864	if (meta == null) {
11865		return null;
11866	}
11867	
11868	// check if is formview
11869	var form = $angular.searchFormView();
11870	if (form && form.form) {
11871		form = form.form;
11872	}
11873	if (form && form.id) {
11874		if (form.root_entity != entity) {
11875			console.log('Differents formview entity: ' + entity + " vs " + form.root_entity);//, meta, form);
11876		} else {
11877			// search in formview columns
11878			if (meta._formsMap == null) {
11879				meta._formsMap = {};
11880			}
11881			if (meta._formsMap[form.id] == null) {
11882				meta._formsMap[form.id] = {};
11883				$entity.loadColumnsMap(entity, form.columns, meta._formsMap[form.id]);
11884			}
11885			var c = meta._formsMap[form.id][path];
11886			if (c != null) {
11887				return c;
11888			}
11889		}
11890	} 
11891	
11892	// search in entity columns
11893	if (meta._columnsMap == null) {
11894		meta._columnsMap = {};
11895		$entity.loadColumnsMap(entity, meta.columns, meta._columnsMap);
11896	}
11897	var column = meta._columnsMap[path];
11898	if (column) {
11899		return column;
11900	}
11901
11902	if (path == "*" || path == "_id") {
11903		return meta.columns[0];
11904	}
11905	if (path.startsWith("_") || path.startsWith("geo_")) {
11906		// no warn
11907	} else {
11908//		$angular.log('Column not found: ' + entity + "#" + path, meta, form);
11909		console.log('Column not found: ' + entity + "#" + path, meta, form);
11910	}
11911	return null; 
11912};
11913
11914$entity.loadColumnsMap = function(entity, columns, columnsMap, path) {
11915	if (columns) {
11916		if (path == null) {
11917			path = "";
11918		}
11919		for (var k in columns) {
11920			
11921			var column = columns[k];
11922			var cpath = column.path;
11923			if (column.type == null && cpath != null) {
11924				// search column in entity
11925				column = $entity.getColumn(entity, cpath);
11926			}
11927			if (column == null || column.type == null) {
11928				continue;
11929			}
11930			
11931			var name = column.name;
11932			if (column.type == 'group') {
11933				var cols = column.columns;
11934				$entity.loadColumnsMap(entity, cols, columnsMap, path);
11935				
11936//			} else if (column.type == 'relation' || column.type == 'relation_entity') {
11937//				var ent = column.param;
11938//				var cols = column.columns;
11939//				
11940			} else if (column.type == 'inner_entity' || column.type == 'inner_entity_list') {
11941				var cols = $entity.getInnerEntityColumns(entity, column);
11942				$entity.loadColumnsMap(entity, cols, columnsMap, path + name + ".");
11943			
11944			} else {
11945			
11946				// save column in map
11947				if (cpath != null) {
11948					columnsMap[cpath] = column;
11949				} else if (name != null) {
11950					columnsMap[path + name] = column;
11951				}
11952			}
11953		}	
11954	}
11955};
11956
11957$entity.getColumnType = function(col) {
11958	if (col.type == 'formula' && col.formula_type) {
11959		return col.formula_type;
11960	}
11961	return col.type;
11962};
11963
11964$entity.getDefaultColumnName = function(entity) {
11965	var col = $entity.getDefaultColumn(entity);
11966	if (col == null) {
11967		return 'name';
11968	}
11969	return col.name;
11970};
11971$entity.getDefaultColumn = function(entity) {
11972	var meta = $entity.getEntity(entity);
11973	if (meta) {
11974		if (meta._col_name == null) {
11975			var cols = $entity.getColumnsBy(meta, 'default');
11976			if (cols.length == 0) {
11977				$angular.log("No default column configured in '" + meta.id + "'");
11978				return null;
11979			}
11980			meta._col_name = cols[0];
11981		}
11982		return meta._col_name;
11983	}
11984	return null;
11985};
11986$entity.getDefaultSort = function(entity) {
11987	var meta = $entity.getEntity(entity);
11988	if (meta) {
11989		return meta.order;
11990	}
11991	return null;
11992};
11993$entity.getTabColumns = function(entity) {
11994	return $entity.getColumnsBy(entity, 'tab');
11995};
11996$entity.getSearchColumns = function(entity) {
11997	return $entity.getColumnsBy(entity, 'search');
11998};
11999$entity.getDisplayColumns = function(entity) {
12000	return $entity.getColumnsBy(entity, 'display');
12001};
12002
12003$entity.getColumnsBy = function(entity, check) {
12004	var meta = $entity.getEntity(entity);
12005	if (meta == null || meta.columns == null) {
12006		return [];
12007	}
12008	if (meta.checkColumns == null) {
12009		meta.checkColumns = {};
12010	}
12011	var entityCheck = meta.id + "#" + check;
12012	if (meta.checkColumns[entityCheck] == null) {
12013		var list = [];
12014		for (var key in meta.columns) {
12015			var column = meta.columns[key];
12016			if ($utils.isTrue(column[check])) {
12017				if (check == 'display' && $constants.isWeb && $utils.isTrue(column.hidden)) {
12018					// no display
12019				} else {
12020					list.push(column);
12021				}
12022			}
12023			if (column.isEntity == null) { // used for search columns
12024				column.isEntity = (column.type === 'entity' || column.type === 'list');
12025			}
12026		}
12027		meta.checkColumns[entityCheck] = list;
12028	}
12029	return meta.checkColumns[entityCheck];
12030};
12031
12032$entity.getTableColumns = function(entity, col) {
12033	if (col && !col.long) {
12034		// return default
12035		var def = $entity.getDefaultColumn(entity);
12036		if (def) {
12037			return [ def ];
12038		}
12039		return [];
12040	}
12041	var meta = $entity.getEntity(entity);
12042	if (meta == null || meta.columns == null) {
12043		return [];
12044	}
12045	if (meta.checkColumns == null) {
12046		meta.checkColumns = {};
12047	}
12048	var entityCheck = entity + "#table";
12049	if (meta.checkColumns[entityCheck] == null) {
12050		var list = $entity.getDisplayColumns(entity).slice(); // clone list
12051		var def = $entity.getDefaultColumn(entity);
12052		//if (def && !list.includes(def)) { -> sometimes no work
12053		if (def != null) {
12054			var found = false;
12055			for (var i in list) {
12056				var item = list[i];
12057				if (def.name == item.name) {
12058					found = true;
12059					break;
12060				}
12061			}
12062			if (!found) {
12063				list.unshift(def);
12064			}
12065		}
12066		meta.checkColumns[entityCheck] = list;
12067	}
12068	return meta.checkColumns[entityCheck];
12069};
12070
12071$entity.metas = {};
12072$entity.getEntityForce = function(entity, callback) {
12073	var o = $entity.getEntity(entity, true);
12074	if (o) {
12075		o = $utils.clone(o);
12076		callback(o);
12077		return;
12078	}
12079	//console.log('getting entity ' + entity);
12080	var url;
12081	if (!$constants.isWeb) {
12082		url = contextPath + '/api/geo/entity/' + entity + '.json';
12083	} else {
12084		url = contextPath + '/www/geo/entity/' + entity + '.json';
12085	}
12086	if ($instance.current) {
12087		url += '?geo_instance='+$instance.getId();
12088	}
12089	$rest.get({
12090		url: url,
12091		force: true,
12092		showError: false
12093	}).then(function(o) {
12094		//console.log('got entity ' + entity, o);
12095		$entity.metas[entity] = o;
12096		o = $utils.clone(o);
12097		callback(o);
12098	});
12099};
12100
12101$entity.getEntity = function(entity, nowarn) {
12102	if ($utils.isBlank(entity)) {
12103		//console.log('entity empty');
12104		return null;
12105	}
12106	if (typeof entity != 'string') {
12107		return entity;
12108	}
12109	if (entity == 'trash') {
12110		return { id: entity, name:entity, columns:[{name:'name', default:true}] };
12111	}
12112	
12113	// check current scope to get fresh data
12114	if ($angular.contentScope) {
12115		var o = $angular.contentScope.meta;
12116		if (o == null && $angular.contentScope.model) {
12117			o = $angular.contentScope.model.meta;
12118		}
12119		if (o && (o.id == entity)) {
12120			if (o.id == entity) {
12121				$entity.metas[entity] = o;
12122			}
12123			return o;
12124		}
12125	}
12126
12127	if (!$instance.hasToRefresh) {
12128		// check instance entities
12129		var o = $instance.getEntity(entity);
12130		if (o) {
12131			$entity.metas[entity] = o;
12132			return o;
12133		}
12134	}
12135
12136	// check in local cache
12137	if ($entity.metas[entity]) {
12138		return $entity.metas[entity];
12139	}
12140
12141	if (nowarn != true) {
12142		var sc = $angular.contentScope;
12143		if (sc) {
12144			if (sc.__entities == null) {
12145				sc.__entities = {};
12146			}
12147			var now = Date.now();
12148			var last = sc.__entities[entity];
12149			if (last && (now - last) < 1000) {
12150				return null; // only warn one time
12151			}
12152			sc.__entities[entity] = now;
12153		}
12154		//console.log('load entity ' + entity);
12155		$entity.getEntityForce(entity, function(){}); // force load entity 
12156	}
12157	return null;
12158};
12159
12160$entity.getInnerEntity = function(entityId, innerEntityId) {
12161	var entity = $entity.getEntity(entityId);
12162	if (entity) {
12163		if ($utils.isObject(innerEntityId) && innerEntityId.name) {
12164			innerEntityId = innerEntityId.name;
12165		}
12166		var map = entity.inner_entities_map;
12167		if (map == null) {
12168			entity.inner_entities_map = {};
12169		}
12170		var ie = entity.inner_entities_map[innerEntityId];
12171		if (ie == null && entity.inner_entities) {
12172			ie = entity.inner_entities.filter(function(innerEntity) {
12173				return innerEntityId === innerEntity.name;
12174			})[0] || null;
12175			entity.inner_entities_map[innerEntityId] = ie;
12176		}
12177		return ie;
12178	}
12179	return null;
12180};
12181
12182$entity.getInnerEntityColumns = function(entityId, column) {
12183	if (column.columns && column.columns.length > 0) {
12184		return column.columns;
12185	}
12186	var innerEntityId = column.inner_entity;
12187	var innerEntity = $entity.getInnerEntity(entityId, innerEntityId);
12188	return innerEntity ? innerEntity.columns : [];
12189};
12190
12191$entity.getTableList = function(o, field, column) {
12192	
12193	if (column.type == 'entity') {
12194		
12195		// parse column.query
12196		var query = $entity.parseQuery(column.query, $angular.contentScope.model);
12197		//console.log(column.param + " - " + query)
12198		
12199		$constants.getDataListMin(column.param, query, 0, 250).then(function(list){
12200			o[field] = list;
12201		});
12202		return;
12203	}
12204	
12205	var name = "";
12206	if (column.type == 'message') {
12207		name = $utils.parseGeoTranslation(column.name);
12208	}
12209	o[field] = [{
12210		name:name
12211	}];
12212};
12213
12214$entity.initTable = function(column) {	
12215	//console.log('init table', column)
12216	delete column.columnX;
12217	delete column.columnY;
12218	delete column.columnsValue;
12219	delete column.tableListX;
12220	delete column.tableListY;
12221	
12222	var columns = column.columns;
12223	if (columns && columns.length > 2) {
12224		
12225		column.columnX = columns[0];
12226		$entity.getTableList(column, 'tableListX', column.columnX);
12227		
12228		column.columnY = columns[1];
12229		$entity.getTableList(column, 'tableListY', column.columnY);
12230
12231		var cols = [];
12232		for (var i=2; i<columns.length; i++) {
12233			cols.push(columns[i]);
12234		}
12235		column.columnsValue = cols;
12236	}
12237		
12238};
12239
12240$entity.cleanTable = function(column, model) {
12241	if (model == null || column == null || column.tableListX == null || column.tableListY == null) {
12242		return;
12243	}
12244	var doc = model[column.name];
12245	if (doc == null) {
12246		return;
12247	}
12248	
12249	var idsx = [];
12250	for (var x in column.tableListX) {
12251		var itemx = column.tableListX[x];
12252		if (itemx.id) {
12253			idsx.push(itemx.id);
12254		} else {
12255			idsx.push("undefined");
12256		}
12257	}
12258	//console.log(idsx)
12259	
12260	var idsy = [];
12261	for (var y in column.tableListY) {
12262		var itemy = column.tableListY[y];
12263		idsy.push(itemy.id);
12264	}
12265	//console.log(idsy)
12266	
12267	for (var x in doc) {
12268		if (x != null && !idsx.includes(x)) {
12269			delete doc[x];
12270			continue;
12271		}
12272		var item = doc[x];
12273		for (var y in item) {
12274			if (!idsy.includes(y)) {
12275				delete item[y];
12276			}
12277		}
12278	}
12279
12280	// fill new values
12281	for (var k in column.tableListX) {
12282		var v1 = column.tableListX[k].id;
12283		if (doc[v1] == null) {
12284			doc[v1] = {};
12285			//console.log('new v1: ' + v1)
12286		}
12287		for (var k2 in column.tableListY) {
12288			var v2 = column.tableListY[k2].id;
12289			if (doc[v1][v2] == null) {
12290				doc[v1][v2] = {};
12291				//console.log('new v2: ' + v1 + " - " + v2)
12292			}
12293		}
12294	}
12295};
12296
12297$entity.isPermitted = function(entityId, permission) {
12298	var entity = $entity.getEntity(entityId);
12299	return entity && entity.security && $utils.isTrue(entity.security[permission]);
12300};
12301
12302$entity.registerCustomEntity = function(entityId, entity) {
12303	$entity.metas[entityId] = entity;
12304};
12305
12306$entity.parseQuery = function(link, model) {
12307	if (link == null) {
12308		link = "";
12309	}
12310	if (model == null) {
12311		model = {};
12312	}
12313	while (link.contains('{')) {
12314		var orig = link;
12315		var index = link.indexOf('{');
12316		var prefix = link.substring(0, index);
12317		link = link.substring(index + 1);
12318		if (!link.contains('}')) {
12319			break;
12320		}
12321		index = link.indexOf('}');
12322		var col = link.substring(0, index).trim();
12323		var suffix = link.substring(index + 1);
12324		var value = model[col];
12325		if (value == null) {
12326			if (col == '_id') {
12327				value = model['id'];
12328			} else if (col == '$instance') {
12329				value = $instance.getId();
12330			}
12331			if (value == null) {
12332				// search in root
12333				var root = $angular.searchRootModel();
12334				if (root) {
12335					value = root[col];
12336				}
12337			}
12338		}
12339		if (value == null) {
12340			value = "";
12341			console.log("var not found " + col + " from " + orig)
12342		}
12343		//console.log(col + "=" + value)
12344		link = prefix + value + suffix;
12345		if (orig === link) {
12346			break;
12347		}
12348	}
12349	return link;
12350};
12351
12352
12353//// File web-agenda.js ////
12354
12355(function() {
12356    
12357	var $agenda = {};
12358	$agenda.selected = null;
12359    
12360    $angular.controllers['web-agenda'] = function($rootScope, $scope, $routeParams, $translate, $uibModal, $window, $document, $location, $timeout) {
12361    	$angular.preparePage({ buttonsRight:"web-agenda-buttons.html" });
12362    	$angular.contentScope = $scope;
12363    	
12364    	$scope.title = "";
12365    	$scope.setTitle = function() {
12366    		$scope.title = $instance.getGeoSmarterText("title_result", "msg.web.agenda");
12367    	};
12368    	$scope.setTitle();
12369    	
12370    	var data = $sessionService.get("agenda_"+$instance.getId());
12371    	if (data == null || $utils.isBlank(data)) {
12372    		$scope.days = [];
12373    	} else {
12374        	$scope.days = JSON.parse(data);
12375    	}
12376    	//console.dir($scope.days)
12377    	$scope.model = {};
12378    	
12379    	$scope.activeDay = 0;
12380    	if ($agenda.selected != null) {
12381    		for (var i=0; i<$scope.days.length; i++) {
12382    			var day = $scope.days[i];
12383    			if (day.date == $agenda.selected.date) {
12384    				$scope.activeDay = i;
12385    			}
12386    		}
12387    	}
12388    	//console.log($scope.activeDay)
12389    	
12390    	$scope.selectDay = function(index) {
12391    		$scope.activeDay = index;
12392    		$scope.model = $scope.days[index];
12393    		if ($scope.showMap) {
12394    			$angular.apply($scope, function() {
12395        			initGeoIndexMap();
12396        		});
12397    		}
12398    	};
12399    	$scope.selectDay($scope.activeDay);
12400
12401    	
12402    	// map
12403    	$scope.showMap = false;
12404    	if ($utils.isTrue($routeParams.__map)) {
12405    		$scope.showMap = true;
12406    		$angular.apply($scope, function() {
12407    			initGeoIndexMap();
12408    		});
12409    	}
12410    	$scope.showOrHideMap = function() {
12411    		$scope.showMap = !$scope.showMap;
12412    		
12413    		if ($scope.showMap) {
12414        		$location.search({'__map':"true"}).replace();
12415    		} else {
12416    			$location.search({}).replace();
12417    		}
12418    		
12419    		if ($scope.showMap) {
12420    			$angular.apply($scope, function() {
12421        			initGeoIndexMap();
12422        		});
12423    		}
12424    	};
12425    	
12426    	var map;
12427    	var infoWindow;
12428    	var markers = {};
12429    	var _tmp = 0;
12430    	function initGeoIndexMap() {
12431    	
12432    		window.scrollTo(0, 0);
12433    		
12434    		markers = {};
12435    		_tmp = 0;
12436    		infoWindow = new google.maps.InfoWindow({ maxWidth: 230, maxHeight: 230 });
12437
12438			var mapOptions = {
12439				mapId : "GEO_AGENDA",
12440				streetViewControl : false, panControl : false,
12441				mapTypeId : google.maps.MapTypeId.ROADMAP,
12442				disableDefaultUI: false, zoomControl: true
12443			};
12444
12445    		var gf = $instance.getGeoFence();
12446    		if (gf && gf.center_latitude) {
12447    			var lat = Number(gf.center_latitude);
12448    			var lng = Number(gf.center_longitude);
12449    			var pos = new google.maps.LatLng(lat, lng);
12450    			var zoom = 12;
12451    			if (gf.default_zoom) {
12452    				zoom = Number(gf.default_zoom);
12453    			}
12454    			mapOptions.center = pos;
12455    			mapOptions.zoom = zoom; 
12456    		} else {
12457    			mapOptions.center = new google.maps.LatLng(0, 0);
12458    			mapOptions.zoom = 3;
12459    		}
12460			map = new google.maps.Map(document.getElementById('web-agenda-map'), mapOptions);
12461			google.maps.event.addListener(map, 'zoom_changed', function() {
12462	            if (this.getZoom() > 17.5 && this.initialZoom == true) {
12463	                this.setZoom(17.5);
12464	                this.initialZoom = false;
12465	            }
12466	        });
12467			
12468			var bounds = new google.maps.LatLngBounds(null);
12469			var num = 0;
12470			for (var id in markers) {
12471				var m = markers[id];
12472				m.setMap(null);
12473			}
12474			markers = {};
12475			
12476			for (var k in $scope.model.list) {
12477				var item = $scope.model.list[k];
12478				console.dir(item)
12479				if (markers[item.object.id]) {
12480					continue; // exists
12481				}
12482	    		// add new
12483				if (item.latitude == null || item.longitude == null) {
12484					continue;
12485				}
12486				var pos = new google.maps.LatLng(item.latitude, item.longitude);
12487				var marker = new google.maps.Marker({
12488					position : pos,
12489					map : map, clickable : true
12490				});
12491				markers[item.object.id] = marker;
12492				
12493				google.maps.event.addListener(marker, 'click', (function(plc, mark) { return function() {
12494					var href = $rootScope.getItemLinkBy(plc.object);
12495					var content = "<div class='infowindow'><a href='"+ href +"' class='bold'>" + $entity.displayColumn(plc.object) + "</a><br>";
12496					content += "<label style='margin-top:5px; font-size: 14px;'>" + $utils.parse(plc.address + "</label></div>");
12497    		    	infoWindow.setContent(content);
12498    		    	infoWindow.open(map, mark);
12499        	    }; })(item, marker));
12500				
12501				bounds.extend(pos);
12502	            num++;
12503			}
12504			
12505			if (num > 0 && map) {
12506				map.initialZoom = true;
12507				map.fitBounds(bounds);
12508			}
12509    	}
12510    	
12511	}
12512
12513})();
12514
12515//// File web-booking.js ////
12516(function() {
12517
12518	$angular.controllers['web-booking'] = function($rootScope, $scope, $routeParams, $translate) {
12519		$angular.preparePage({buttonsRight: "web-booking-buttons.html"});
12520		$angular.contentScope = $scope;
12521
12522		$scope.title = "";
12523		$scope.setTitle = function() {
12524			$scope.title = $translate.instant('msg.booking') + ' ' + $scope.model.id;
12525		};
12526
12527		function loadBooking() {
12528			$rest.get({
12529				skipHideLoading: true,
12530				loading: "generic",
12531				url: contextPath + "/www/booking/" + $routeParams.id + ".json"
12532			}).then(function(o) {
12533				$scope.model = o;
12534				$scope.setTitle();
12535				$rest.get({
12536					skipHideLoading: true,
12537					loading: "generic",
12538					url: contextPath + "/www/booking/a/columns.json?entity=" + o.object.entity + "&entityId=" + o.object.entity_id
12539				}).then(function(bookingEntity) {
12540					$scope.extraDataEntity = bookingEntity;
12541
12542					$angular.apply($scope, function() {
12543						$angular.ready = true;
12544					});
12545				});
12546			});
12547		}
12548
12549		loadBooking();
12550
12551		$scope.cancelBooking = function() {
12552			$rest.remove({
12553				url: contextPath + "/www/booking/" + $scope.model.id + ".json",
12554				confirmTitle: 'msg.booking.cancel.confirm.title',
12555				confirmText: 'msg.booking.cancel.confirm.text',
12556				loading: "generic"
12557			}).then(loadBooking);
12558		};
12559
12560		$scope.buildAddToGoogleCalendarLink = function() {
12561			var booking = $scope.model;
12562			var day = booking.day.replaceAll('-', '');
12563			var timeFrom = booking.time.replace(':', '');
12564			var timeTo = booking.end_time ? booking.end_time.replace(':', '') : '';
12565			return 'https://www.google.com/calendar/render' +
12566				'?action=TEMPLATE' +
12567				'&text=' + encodeURIComponent($constants.getEntityDesc(booking.object.entity, booking.object.entity_id)) +
12568				'&dates=' + day + 'T' + timeFrom + '00/' + day + 'T' + timeTo + '00';
12569		};
12570
12571		$scope.buildAddToOutlookCalendarLink = function() {
12572			var booking = $scope.model;
12573			return 'https://outlook.live.com/calendar/0/deeplink/compose' +
12574				'?path=/calendar/action/compose' +
12575				'&rru=addevent' +
12576				'&startdt=' + booking.day + 'T' + booking.time + ':00' +
12577				(booking.end_time ? '&enddt=' + booking.day + 'T' + booking.end_time + ':00' : '') +
12578				'&subject=' + encodeURIComponent($constants.getEntityDesc(booking.object.entity, booking.object.entity_id));
12579		};
12580	}
12581})();
12582
12583//// File web-cart.js ////
12584(function() { 
12585	
12586	$angular.controllers['web-cart'] = function($rootScope, $scope, $routeParams, $translate, $location, $timeout) {
12587		$angular.preparePage({buttonsRight: "web-cart-buttons.html"});
12588		$angular.contentScope = $scope;
12589
12590		$scope.showNext = false;
12591		$scope.title = "";
12592
12593		$scope.setTitle = function() {
12594			if ($scope.step === 1) {
12595				$scope.title = $translate.instant("msg.web.cart");
12596			} else if ($scope.step === 2) {
12597				$scope.title = $translate.instant("msg.buyer");
12598			} else if ($scope.step === 3) {
12599				$scope.title = $translate.instant("msg.delivery");
12600			} else if ($scope.step === 4) {
12601				$scope.title = $translate.instant("msg.identity");
12602			} else if ($scope.step === 5) {
12603				$scope.title = $translate.instant("msg.pay.mode");
12604			}
12605		};
12606
12607		if ($angular.currencies && $instance.current && $instance.current.geo_buy) {
12608			$scope.currencyConfig = $angular.currencies[$instance.current.geo_buy.currency];
12609		}
12610
12611		$scope.checkout = function() {
12612			if ($scope.validateCheckout) {
12613				$scope.validateCheckout();
12614			} else {
12615				goToStep($scope.step + 1);
12616			}
12617		};
12618		$scope.back = function() {
12619			goToStep($scope.step - 1, true);
12620		};
12621
12622		function goToStep(step, goBack) {
12623			//console.log('going to step ' + step)
12624			
12625			$scope.isDirectPay = false;
12626			if ($rootScope.cart == null || $rootScope.cart.items == null || $rootScope.cart.items.length == 0) {
12627				step = 1;
12628			} else {
12629				var extra = $rootScope.cart.items[0].extra_data;
12630				$scope.isDirectPay = extra != null && extra.directPay == true;
12631				if ($scope.isDirectPay && step < 2) {
12632					step = 2;
12633				}
12634			}
12635
12636			if (step === 5) {
12637				// force start again
12638				$scope.payment_id = null; 
12639			}
12640			
12641			if (step === 6) {
12642				// start payment if needed
12643				if ($scope.payment_id == null) {
12644					
12645					$scope.startPayment();
12646					return;			
12647				}
12648				goToStep(7);
12649				return;
12650			}
12651			
12652			if (step === 7) {
12653				// end
12654				console.log('cart', $rootScope.cart);
12655				
12656				if ($scope.payment_id == null) {
12657					if ($rootScope.cart && $rootScope.cart.payment) {
12658						$scope.payment_id = $rootScope.cart.payment;
12659					}
12660				}
12661				if ($scope.payment_id != null) {
12662					finishPayment();
12663					return;
12664				}
12665				goToStep(5);
12666				return;
12667			}
12668			
12669			isValidStep(step, function(ok) {
12670				//console.log('step is valid: ' + ok)
12671				if (ok || step === 5) {
12672					// step is valid: show it
12673					$scope.step = step;
12674					$scope.setTitle();
12675					prepareStep();
12676					return;
12677				}
12678				
12679				// try next
12680				var tryStep = goBack ? (step - 1) : (step + 1);
12681				goToStep(tryStep, goBack);
12682			});
12683		}
12684
12685		$scope.choosePaymentMethod = function(methodName) {
12686			$scope.selectedPaymentMethod = methodName;
12687			if (methodName === 'braintree' || methodName === 'ath_movil_business' || methodName === 'paypal' || methodName === 'ath_movil_business') {
12688				$scope.choosePayment = false;
12689				$scope.startPayment();
12690				return;
12691			}
12692			$scope.showNext = true;
12693		};
12694		
12695		$scope.startPayment = function() {
12696			var methodName = $scope.selectedPaymentMethod;
12697			
12698			if (methodName === 'braintree') {
12699				prepareExternalPayment('https://js.braintreegateway.com/web/dropin/1.22.1/js/dropin.min.js', prepareBraintreePayment);
12700			
12701			} else if (methodName === 'stripe') {
12702				prepareExternalPayment('https://js.stripe.com/v3/', prepareStripePayment);
12703			
12704			} else if (methodName === 'paypal') {
12705				prepareExternalPayment(function(data) {
12706					return 'https://www.paypal.com/sdk/js?client-id=' + data.client_id;
12707				}, preparePaypalPayment);
12708			
12709			} else if (methodName === 'ath_movil_business') {
12710				prepareExternalPayment('https://www.athmovil.com/api/js/v3/athmovilV3.js', prepareAthMovilBusinessPayment, true);
12711			
12712			} else if (methodName === 'cybersource' || methodName === 'authorize_net' || methodName === 'pharos') {
12713				prepareExternalPayment(null, prepareRedirectPayment);
12714			
12715			} else if (methodName === 'fake_payment') {
12716				prepareExternalPayment(null, prepareFakePayment);
12717			
12718			} else {
12719				console.log('Unknown payment method: ' + methodName);
12720				goToStep(5);
12721			}
12722		};
12723
12724		$scope.chooseDeliveryMethod = function(method, force) {
12725			var deliveryMethodChanged = !$cart.isDeliveryMethodType(method ? method.name : null);
12726			if (force || deliveryMethodChanged) {
12727				$scope.validateCheckout = null;
12728				if (method) {
12729					if (deliveryMethodChanged) {
12730						$cart.setDeliveryMethod({
12731							type: method.name,
12732							price: $scope.getDeliveryMethodPrice(method)
12733						});
12734					}
12735					if (method.entity) {
12736						if (method.entityId) {
12737							prepareDeliveryMethodBooking(method);
12738						} else {
12739							prepareDeliveryMethodForm(method);
12740						}
12741					} else {
12742						$scope.showNext = true;
12743					}
12744				} else {
12745					$cart.setDeliveryMethod(null);
12746					$scope.showNext = false;
12747				}
12748			}
12749		}
12750
12751		$scope.getDeliveryMethodPrice = function(method) {
12752			return method.price && !isNaN(method.price) ? Number(method.price) : null;
12753		};
12754
12755		function prepareExternalPayment(scriptUrl, callback, loadScriptAfterCallback) {
12756			$cart.pay({
12757				payment_method: $scope.selectedPaymentMethod,
12758				location: $location.absUrl()
12759			}).then(function(o) {
12760				var methodName = $scope.selectedPaymentMethod;
12761				var script = $cart.externalPaymentScripts[methodName];
12762				var paymentMethodData = o.data;
12763				$scope.payment_id = o.payment_id;
12764				var runCallback = true;
12765				if (loadScriptAfterCallback) {
12766					callback(paymentMethodData, null);
12767					runCallback = false;
12768				}
12769				if (scriptUrl != null && script == null) {
12770					if (typeof scriptUrl === 'function') {
12771						scriptUrl = scriptUrl(paymentMethodData);
12772					}
12773					require([scriptUrl], function(dropin) {
12774						$cart.externalPaymentScripts[methodName] = dropin;
12775						$utils.hideLoading();
12776						if (runCallback) {
12777							callback(paymentMethodData, dropin);
12778						}
12779					});
12780				} else {
12781					$utils.hideLoading();
12782					if (runCallback) {
12783						callback(paymentMethodData, script);
12784					}
12785				}
12786			});
12787		}
12788
12789		var payInstance;
12790		function prepareBraintreePayment(data, braintree) {
12791			braintree.create({
12792				authorization: data.client_token,
12793				container: '#external-payment-container'
12794			}, function(createErr, instance) {
12795				payInstance = instance;
12796				if (payInstance == null) {
12797					$utils.floatingError($translate.instant("msg.error"));
12798					return;
12799				}
12800				$scope.showValidateBtn = true;
12801				$angular.apply($scope);
12802			});
12803		}
12804
12805		function prepareStripePayment(data) {
12806			var stripe = Stripe(data.publishable_key);
12807			stripe.redirectToCheckout({
12808				sessionId: data.session_id
12809			});
12810			showLoading();
12811		}
12812
12813		function preparePaypalPayment(data) {
12814			paypal.Buttons({
12815				createOrder: function() {
12816					return Promise.resolve(data.order_id);
12817				},
12818				onApprove: function() {
12819					$scope.checkout();
12820				}
12821			}).render('#external-payment-container');
12822		}
12823
12824		function prepareAthMovilBusinessPayment(data) {
12825			var $external = $('#external-payment-container');
12826			$external.empty();
12827			$external.append('<div style="width: 100px;"><div id="ATHMovil_Checkout_Button"></div></div>');
12828			window.ATHM_Checkout = {
12829				env: data.environment,
12830				publicToken: data.public_key,
12831				theme: 'btn',
12832				lang: $locale.language,
12833				total: $cart.getTotalAmount(),
12834				onCompletedPayment: function(response) {
12835					if (response.status === 'completed') {
12836						$scope.externalPaymentData.athMovilBusinessReferenceNumber = response.referenceNumber;
12837						$scope.checkout();
12838					}
12839				},
12840				onCancelledPayment: function() {
12841					$location.reload();
12842				},
12843				onExpiredPayment: function() {
12844					$location.reload();
12845				}
12846			}
12847		}
12848
12849		function prepareFakePayment(data) {
12850			$scope.checkout();
12851		}
12852
12853		function prepareRedirectPayment(data) {
12854			
12855			// test cybersource
12856//			var url = contextPath + "/ms/a/paid.go";
12857//			$rest.post({
12858//				loading: "generic",
12859//				url: url,
12860//				data: {
12861//					req_reference_number:data.reference_number,
12862//					transaction_id: '12345678', reason_code:'100'
12863//				}
12864//			}).then(function() {
12865//				window.location.href = url;
12866//			}).catch(function(e) {
12867//				console.log('error', e)
12868//				goToStep(5);
12869//			});
12870			
12871			var url = data.redirect_url;
12872			delete data.redirect_url;
12873			var form = '<form method="post" action="' + url + '" class="hidden">';
12874			for (var k in data) {
12875				form += '\n<input name="' + k + '" value="' + data[k] + '" />';
12876			}
12877			form += '\n</form>';
12878			//console.log(form)
12879			submitPaymentForm(form);
12880		}
12881		
12882		function submitPaymentForm(form) {
12883			var $form = $(form);
12884			var $external = $('#external-payment-container');
12885			$external.empty();
12886			$external.append($form);
12887			$form.submit();
12888			showLoading();
12889		}
12890		
12891		function showLoading() {
12892			$scope.payment_id = null; // important to wait for payment
12893			$utils.showLoading();
12894			setTimeout(function() {
12895				$utils.hideLoading();
12896			}, 3000);
12897		}
12898
12899//		function showPaymentButton(onClick) {
12900//			var $external = $('#external-payment-container');
12901//			$external.empty();
12902//			var $button = $('<button class="btn btn-primary">' + $translate.instant('msg.goToPay') + '</button>');
12903//			$external.append($button);
12904//			$button.click(onClick);
12905//		}
12906
12907		$scope.validateCard = function() {
12908			if ($scope.selectedPaymentMethod === 'braintree') {
12909				payInstance.requestPaymentMethod(function(err, p) {
12910					if (p == null) {
12911						$utils.floatingError($translate.instant("msg.error"));
12912						return;
12913					}
12914					$scope.externalPaymentData.braintreeNonce = p.nonce;
12915					$scope.showValidateBtn = false;
12916					$scope.showNext = true;
12917					$angular.apply($scope);
12918				});
12919				return;
12920			}
12921			$scope.showValidateBtn = false;
12922			$scope.showNext = true;
12923			$angular.apply($scope);
12924		};
12925
12926		function finishPayment() {
12927			$cart.complete({
12928				payment_id: $scope.payment_id,
12929				data: $scope.externalPaymentData
12930			}).then(function(order) {
12931				if (order == null) {
12932					$angular.log('Payment 4 failed');
12933					$utils.showAlert($translate.instant("msg.payment.failed"));
12934					return;
12935				}
12936				$angular.log('Payment 4 done');
12937				$location.path($instance.getBaseUrl() + 'order/' + order.id).search({isNew: true, code: order.code});
12938			});
12939		}
12940
12941		$scope.availableDeliveryMethods = [];
12942		if ($instance.current && $instance.current.geo_buy && $instance.current.geo_buy.delivery_methods) {
12943			$scope.availableDeliveryMethods = $instance.current.geo_buy.delivery_methods.filter(function(method) {
12944				return !!method.name;
12945			});
12946		}
12947
12948		function prepareStep() {
12949			if ($scope.step === 1) {
12950				$scope.showNext = true;
12951			}
12952			if ($scope.step > 1) {
12953				$rest.get({
12954					skipHideLoading: true,
12955					loading: "generic",
12956					url: contextPath + "/www/user/a/mycart.json"
12957				}).then(function(o) {
12958					$scope.user = o;
12959				});
12960			}
12961			if ($scope.step === 2) {
12962				$scope.showNext = true;
12963				return;
12964			}
12965			if ($scope.step === 3) {
12966				if ($scope.availableDeliveryMethods.length === 0) {
12967					$cart.setDeliveryMethod(null);
12968					$scope.checkout();
12969				} else if ($scope.availableDeliveryMethods.length === 1) {
12970					$scope.chooseDeliveryMethod($scope.availableDeliveryMethods[0], true);
12971				} else {
12972					$scope.chooseDeliveryMethod($scope.availableDeliveryMethods.filter(function(method) {
12973						return $cart.isDeliveryMethodType(method.name);
12974					})[0] || null, true);
12975				}
12976				return;
12977			}
12978			if ($scope.step === 4) {
12979				$scope.identity = {
12980					checkoutAs: $cart.isAllowedAnonymous() && $cart.hasEmail() ? 'guest' : 'user'
12981				};
12982				$scope.showNext = isIdentityReadyToContinue();
12983				return;
12984			}
12985			if ($scope.step === 5) {
12986				if ($instance.current && $instance.current.geo_buy && $utils.isTrue($instance.current.geo_buy.disabled_payment)) {
12987					$scope.checkout();
12988					return;
12989				}
12990					
12991				$rest.get({
12992					skipHideLoading: true,
12993					url: contextPath + "/www/payment-methods.json",
12994					loading: "generic"
12995				}).then(function(o) {
12996					$scope.choosePayment = true;
12997					$scope.availablePaymentMethods = o.methods;
12998					$scope.externalPaymentData = {};
12999					
13000					if ($scope.availablePaymentMethods.length === 1) {
13001						$scope.choosePaymentMethod($scope.availablePaymentMethods[0]);
13002					}
13003				});
13004			}
13005		}
13006
13007		function prepareDeliveryMethodBooking(method) {
13008			$rest.get({
13009				url: contextPath + '/www/geo/' + method.entity + '/' + method.entityId + '.json',
13010				loading: 'generic'
13011			}).then(function(geoObject) {
13012				if (geoObject.geo_schedule && geoObject.geo_schedule.booking && geoObject.geo_schedule.booking.enabled) {
13013					method.bookingConfig = {
13014						geoObject: geoObject,
13015						model: $cart.getDeliveryMethodBooking()
13016					};
13017				} else {
13018					$scope.showNext = true;
13019				}
13020			});
13021		}
13022
13023		$scope.onChangeDeliveryMethodBooking = function(booking, isReady) {
13024			$scope.showNext = isReady;
13025			$cart.setDeliveryMethodBooking(booking);
13026		};
13027
13028		function prepareDeliveryMethodForm(method) {
13029			var formEntity = method.entity;
13030			if ($entity.isGeoForm(formEntity) && $entity.isPermitted(formEntity, 'create')) {
13031				var entity = $entity.getEntity(formEntity);
13032				if (entity) {
13033					$scope.validateCheckout = function() {
13034						$scope.$broadcast('validateDeliveryMethodExtraData');
13035					};
13036					method.formConfig = {
13037						entity: entity,
13038						model: $cart.getDeliveryMethodExtraData(),
13039						onSubmit: function(formModel) {
13040							$cart.setDeliveryMethodExtraData(formEntity, formModel);
13041							$scope.validateCheckout = null;
13042							$scope.checkout();
13043						}
13044					};
13045				}
13046			}
13047			$scope.showNext = true;
13048		}
13049
13050		$scope.onChangeIdentity = function() {
13051			if ($user.isLogged()) {
13052				$cart.doWhenCartWasInitialized(function() {
13053					$scope.checkout();
13054				});
13055			} else {
13056				$cart.save();
13057				$scope.showNext = isIdentityReadyToContinue();
13058			}
13059		};
13060
13061		function isIdentityReadyToContinue() {
13062			return $cart.hasEmail() && $scope.identity.checkoutAs === 'guest';
13063		}
13064
13065		function isValidStep(step, callback) {
13066			if (step === 1) {
13067				callback(true);
13068				return;
13069			}
13070			if (step === 2) {
13071				if ($scope.user) {
13072					callback($scope.user.users_meta.length > 0 || $scope.user.users.length > 0);
13073				} else if ($user.isLogged()) {
13074					isUserDataStepValid(callback);
13075				} else {
13076					callback(false);
13077				}
13078				return;
13079			}
13080			if (step === 3) {
13081				callback($scope.availableDeliveryMethods.length > 0);
13082				return;
13083			}
13084			if (step === 4) {
13085				callback(!$user.isLogged());
13086				return;
13087			}
13088			if (step === 5) {
13089				callback(($instance.current && $instance.current.geo_buy && $utils.isTrue($instance.current.geo_buy.disabled_payment)) 
13090					|| $cart.getTotalAmount() > 0);
13091				return;
13092			}
13093			$angular.goHome();
13094		}
13095
13096		function isUserDataStepValid(callback) {
13097			$rest.get({
13098				skipHideLoading: true,
13099				loading: "generic",
13100				url: contextPath + "/www/user/a/mycart.json"
13101			}).then(function(o) {
13102				callback(o.users_meta.length > 0 || o.users.length > 0);
13103			});
13104		}
13105
13106		$cart.doWhenCartWasInitialized(function() {
13107		
13108			if ($routeParams.entity && $routeParams.entityId) {
13109				
13110				// clean cart
13111				$cart.resetRaw();
13112					
13113				// check if is booking			
13114				if ($routeParams.entity == 'booking') {
13115					
13116					// load booking
13117					var url = contextPath + '/www/booking/' + $routeParams.entityId + '.json';
13118					$rest.get({
13119						skipHideLoading: true,
13120						url: url,
13121						loading: 'generic'
13122					}).then(function(o) {
13123						
13124						// load item
13125						var url2 = contextPath + '/www/geo/' + o.object.entity + '/' + o.object.entity_id + '.json';
13126						$rest.get({
13127							skipHideLoading: true,
13128							url: url2,
13129							loading: 'generic'
13130						}).then(function(item) {
13131							$cart.addItem(item.entity, item.id, { quantity: 1, extraData: { booking:o.id, directPay:true }});
13132							goToStep(2);
13133						});
13134					});
13135					return;
13136				}
13137				 
13138				// get cart item
13139				var url = contextPath + '/www/geo/' + $routeParams.entity + '/' + $routeParams.entityId + '.json';
13140				$rest.get({
13141					skipHideLoading: true,
13142					url: url,
13143					loading: 'generic'
13144				}).then(function(item) {
13145					if (!$cart.isCartItem(item, true)) {
13146						console.log('no cart item', item);
13147					} else {
13148						var hasStock = $cart.hasStock(item);
13149						if (!hasStock) {
13150							console.log('no stock', item); 
13151						} else {
13152							$cart.addItem(item.entity, item.id, { quantity: 1, extraData: { directPay:true }});
13153						}
13154					}
13155					goToStep(2);
13156				});
13157				return;
13158			}
13159			
13160			var successPayment = $location.search().success_payment;
13161			if (!$utils.isBlank(successPayment)) {
13162				// success
13163				$scope.payment_id = successPayment;
13164				$scope.step = 6;
13165				$scope.checkout();
13166				return;
13167			}
13168
13169			var failedPayment = $utils.isTrue($location.search().failed_payment);
13170			if (failedPayment) {
13171				// failed
13172				$utils.showAlert($translate.instant("msg.payment.failed"));
13173				return;
13174			}
13175
13176			// step
13177			var step = 1;
13178			if (!$utils.isBlank($location.search().step)) {
13179                step = Number($location.search().step);
13180				if (isNaN($location.search().step)) {
13181					console.log('error getting step ' + $location.search().step)
13182					step = 1;
13183				}
13184				var search = $location.search();
13185				delete search.step;
13186				$location.search(search).replace();
13187			}
13188			goToStep(step);
13189		});
13190	}
13191})();
13192
13193//// File web-plan.js ////
13194(function() {
13195    
13196    
13197    $angular.controllers['web-plan'] = function($rootScope, $scope, $routeParams, $translate, $uibModal, $window, $document, $location, $timeout) {
13198    	$angular.preparePage({buttonsRight:"web-plan-buttons.html"});
13199    	$angular.contentScope = $scope;
13200    	
13201    	$scope.title = "";
13202    	$scope.setTitle = function() {
13203    		$scope.title = $instance.getGeoSmarterText("title_plan", "msg.web.plan");
13204    	};
13205    	$scope.setTitle();
13206    	
13207    	//var page = $location.path();
13208    	//$events.lastScrolls[page] = 0;
13209    	
13210    	if ($instance.current.geo_smarter == null) {
13211    		$instance.current.geo_smarter = {};
13212    	}
13213        if ($utils.isTrue($instance.current.geo_smarter.disabled)) {
13214        	$utils.showAccessDenied();
13215			return;
13216    	}
13217    	if ($instance.current.geo_smarter.types == null) {
13218    		$instance.current.geo_smarter.types = [];
13219    	}
13220    	if ($instance.current.geo_smarter.people == null) {
13221    		$instance.current.geo_smarter.people = [];
13222    	}
13223    	if ($instance.current.geo_smarter.categories == null) {
13224    		$instance.current.geo_smarter.categories = [];
13225    	}
13226    	if ($instance.current.subInstances == null) {
13227    		$instance.current.subInstances = [];
13228    	}
13229    	
13230    	$scope.model = {
13231    		date_from: $utils.fromDate(new Date(), $constants.formatDay),
13232    		date_to: $utils.fromDate(new Date(), $constants.formatDay),
13233    		people: {},
13234    		types: [],
13235    		instances: [],
13236    		categories: []
13237    	};
13238    	$scope.other = {
13239    		showAll : false	
13240    	};
13241    	
13242    	if ($instance.current.geo_smarter.types.length > 0) {
13243    		$scope.model.types.push($instance.current.geo_smarter.types[0]);
13244    	}
13245    	
13246        $scope.selectAll = function(list) {
13247        	list.splice(0, list.length);
13248        };
13249        $scope.clickChip = function(list, item, noMultiple) {
13250        	if (list.indexOf(item) == -1) {
13251            	if (noMultiple) {
13252            		while(list.length > 0) {
13253            		    list.pop();
13254            		}
13255            	}
13256        		list.push(item);
13257        	} else {
13258        		$utils.removeFromArray(list, item);
13259        	}
13260        };
13261        
13262        $scope.clickPerson = function(type) {
13263        	var people = $scope.model.people;
13264        	if (people[type] == null) {
13265        		people[type] = 0;
13266        	}
13267        	people[type]++;
13268        	$angular.apply($scope);
13269        };
13270        $scope.removePerson = function(e, type) {
13271        	$utils.preventDefault(e);
13272        	var people = $scope.model.people;
13273        	people[type]--;
13274        	$angular.apply($scope);
13275        };
13276        $scope.getNumber = function(value) {
13277        	if (value) {
13278        		return value;
13279        	}
13280        	return 0;
13281        }
13282        
13283        $scope.finish = function()  {
13284        	
13285        	var model = JSON.parse(JSON.stringify($scope.model));
13286        	if ($scope.model.types.length > 0) {
13287        		model.type = $scope.model.types[0]; 
13288        	}
13289        	delete model.types;
13290        	//console.dir(model)
13291        	
13292        	$rest.post({
13293        		url: contextPath + "/www/smarter.json",
13294        		data: model,
13295        		loading: "generic"
13296        	}).then(function(data) {
13297        		if (data == null || data.length == 0) {
13298        			var txt = $translate.instant("msg.web.agenda.noResults");
13299        			$utils.floatingError(txt);
13300        			return;
13301        		}
13302        		//console.dir(data)
13303        		
13304        		// save data
13305        		$sessionService.put("agenda_"+$instance.getId(), JSON.stringify(data));
13306        		
13307        		$location.path($instance.getBaseUrl() + "agenda").replace();
13308        	});
13309        	
13310        };
13311    	
13312	}
13313
13314})();
13315
13316//// File web-user.js ////
13317(function() {
13318    
13319    
13320    $angular.controllers['web-user'] = function($rootScope, $scope, $translate, $location) {
13321    	$angular.preparePage();
13322    	$angular.contentScope = $scope;
13323    	
13324    	$scope.title = "";
13325    	$scope.setTitle = function() {
13326        	$scope.title = $translate.instant("msg.user.account");
13327    	};
13328    	$scope.setTitle();
13329    	
13330    	$scope.model = {};
13331		$scope.showEdit = true;    	
13332    	$rest.get({
13333			skipHideLoading: true,
13334			loading: "generic",
13335			url: contextPath + "/www/user/a/myaccount.json"
13336		}).then(function(o) {
13337			$scope.model = o;
13338			
13339			$angular.apply($scope, function() {
13340				$angular.ready = true;
13341			});
13342		});
13343    	
13344	}
13345	
13346})();
13347
13348//// File web-home.js ////
13349(function() {
13350    
13351    
13352    $angular.controllers['web-home'] = function($rootScope, $scope, $routeParam
13352s, $translate, $window, $location, geolocationService, $timeout) {
13353    	$angular.preparePage({ resetBacks:true, buttonsRight:"web-home-buttons.html" });
13354    	$angular.contentScope = $scope;
13355    	
13356    	if ($instance.current == null) {
13357    		$angular.log('no instance');
13358    		return;
13359    	}
13360    	
13361    	function initHome() {
13362    		
13363    		var id = $routeParams.id;
13364    		if ($utils.isBlank(id)) {
13365            	// get first page
13366    			var o = $instance.current.geo_web;
13367    			if (o && o.menu && o.menu.length > 0) {
13368    				id = o.menu[0].id;
13369					$location.path($instance.getBaseUrl() + "p/" + id).search({}).replace();
13370					return;
13371    			}
13372    		}
13373    		if ($utils.isBlank(id)) {
13374    			$angular.log('no page id found');
13375    			return;
13376    		}
13377    		
13378			$angular.menuScope.selectedPage = 'home-' + id;
13379    		$scope.page = null;
13380    		$rest.get({
13381    			skipHideLoading: true,
13382    			loading: "generic",
13383    			url: contextPath + "/www/instance/"+$instance.current.id+"/home.json?page=" + id
13384    		}).then(function(o) {
13385    			$scope.page = o;
13386    			parseItems();
13387				
13388				$angular.apply($scope, function() {
13389					var url = window.location.href;
13390					if (url.contains("#")) {
13391						var id = url.split("#")[1];
13392						if (!$utils.isBlank(id)) {
13393							var el = document.getElementById(id);
13394							if (el !== null) {
13395								el.scrollIntoView();
13396								return;
13397							}
13398						}
13399					}
13400					$angular.ready = true;
13401				});
13402    		});
13403    	}
13404    	
13405    	function parseItems() {
13406    		var items = [];
13407			var requestGeoLocation = false;
13408    		if ($scope.page.items) {
13409    			var slides = [];
13410    			var gallery = [];
13411    			for (var k in $scope.page.items) {
13412    				var item = $scope.page.items[k];
13413    				if ($utils.isTrue(item.hidden)) {
13414    					continue;
13415    				}
13416    				loadData(item);
13417    				
13418					if (item.classes == null) {
13419						item.classes = "";
13420					}
13421    				if (item.type != 'carousel' && item.type != 'preview-carousel' && item.type != 'gallery') {
13422						if (!item.classes.contains("col-")) {
13423    						if (item.type == 'column') {
13424	        					item.classes += " col-sm-4";
13425    						} else {
13426	        					item.classes += " col-12";
13427	    					}
13428    					}
13429    				}
13430					if (!$utils.isBlank(item.alignment)) {
13431						item.classes += " align-" + item.alignment;
13432					}
13433    				
13434    				// check carousel
13435    				if (item.type == 'carousel') {
13436    					slides.push(item);
13437    					continue;
13438    				}
13439    				if (slides.length > 0) {
13440    					items.push({
13441    						type: 'carousel',
13442    						classes: "col-12",
13443    						list: slides
13444    					});
13445    					slides = [];
13446    				}
13447    				
13448    				// check gallery
13449    				if (item.type == 'gallery') {
13450    					gallery.push(item);
13451    					continue;
13452    				}
13453    				if (gallery.length > 0) {
13454    					items.push({
13455    						type: 'gallery',
13456    						classes: "col-12",
13457    						list: gallery
13458    					});
13459    					gallery = [];
13460    				}
13461
13462    				// TODO remove title / horizontal
13463    				if ($utils.isBlank(item.type) || item.type == 'title' || item.type == 'horizontal') { 
13464    					item.type = 'default';
13465    				}
13466    				
13467    				// normal item
13468    				items.push(item);
13469    			}
13470				
13471    			if (slides.length > 0) {
13472					items.push({
13473						type: 'carousel',
13474						classes: "col-12",
13475						list: slides
13476					});
13477				}
13478    			if (gallery.length > 0) {
13479					items.push({
13480						type: 'gallery',
13481						classes: "col-12",
13482						list: gallery
13483					});
13484				}
13485
13486				if ((item.type === 'form' || item.type === 'formview') && item.entity && $entity.isAutoLocation(item.entity)) {
13487					requestGeoLocation = true;
13488				}
13489    		}
13490
13491			if (requestGeoLocation) {
13492				geolocationService.getCurrentLocation().then(function(location) {
13493					$scope.currentGeoLocation = location;
13494				});
13495			}
13496
13497    		$scope.page.itemList = items;
13498    	}
13499    	
13500    	var webForm = null;
13501    	var webFormCallback = null;
13502    	$scope.webFormItem = null;
13503    	$scope.initForm = function(form, item) {
13504    		webForm = form;
13505    		$scope.webFormItem = item;
13506    	}
13507    	$scope.saveForm = function(callback) {
13508    		webFormCallback = callback;
13509			setTimeout(function(){
13510    			$("#webForm")[0].requestSubmit(); // to validate form
13511			}, 100);
13512    	}
13513    	$scope.submitForm = function(e) {
13514    		$utils.preventDefault(e);
13515    		
13516			if (!$utils.validateForm(webForm.webForm)) {
13517				return;
13518			}
13519			
13520			var item = $scope.webFormItem;
13521			var model = item.model;
13522			var isCreate = item.model.id == null;
13523			
13524			if (item.type == 'formview') {
13525				isCreate = item.form.data.id == null;
13526				model = {
13527					id: item.form.data.id,
13528	        		formId : item.form.form.id,
13529	        		data: item.form.data
13530	        	};
13531			}
13532
13533			if ($scope.currentGeoLocation) {
13534				model.geo_location = {
13535					latitude: $scope.currentGeoLocation.latitude,
13536					longitude: $scope.currentGeoLocation.longitude
13537				};
13538			}
13539			
13540			$rest.post({
13541				url: contextPath + "/www/geo/" + item.entity + ".json",
13542				data: model,
13543				loading: "generic"
13544			}).then(function(data) {
13545			
13546				if (isCreate && item.form != null) {
13547					if (item.type == 'formview') {
13548						item.form.data.id = data.id;
13549					} else {
13550						item.form.model.id = data.id;
13551					}
13552				}
13553				$utils.postSave(model, data, isCreate, webFormCallback, function(callback) {
13554					
13555					if (!$utils.isBlank(item.link)) {
13556		    			var url = item.link;
13557		    			if (url.contains('geodesignlabs.com/admin')) {
13558		    				
13559		    			} else if (url.contains('geodesignlabs.com/') && !url.contains('/media')) {
13560		    				url = url.split('geodesignlabs.com/')[1];
13561		    			}
13562						url = $entity.parseQuery(url, data);
13563		    			$location.path(url);
13564		    			
13565		    			if (callback) {
13566		    				callback();
13567		    			}
13568		    			return;
13569		    		}
13570					
13571					// reload data
13572					loadForm(item, data);
13573					
13574					if (callback) {
13575	    				callback();
13576	    			}
13577				});
13578			});
13579		};
13580		
13581		$scope.refreshForm = function() {
13582			refreshForm();
13583		};
13584		function refreshForm() {
13585			loadForm($scope.webFormItem, { id:$scope.webFormItem.model.id });
13586		}
13587		
13588		$scope.resetForm = function() {
13589			// reload with new
13590			loadForm($scope.webFormItem);
13591		};
13592
13593		$scope.openUploadFile = function(e) {
13594			$utils.preventDefault(e);
13595			$('#webFormAttachments input[type="file"]').trigger('click');
13596		};
13597
13598		$scope.uploadFile = function(files) {
13599			if (files.length === 0) {
13600				return;
13601			}
13602			var file = files[0];
13603			var reader = new FileReader();
13604			reader.onload = function(event) {
13605				$rest.post({
13606					url: contextPath + '/www/media/a/createBase64.json',
13607					data: {
13608						fileBase64: event.target.result.split(',')[1],
13609						fileName: file.name,
13610						entity: $scope.webFormItem.entity,
13611						entityId: $scope.webFormItem.model.id
13612					},
13613					loading: 'generic',
13614					successMessage: $translate.instant('msg.saved')
13615				}).then(function() {
13616					refreshForm();
13617				});
13618			};
13619			reader.readAsDataURL(file);
13620		};
13621
13622		$scope.removeFile = function(item, item2) {
13623			$rest.remove({
13624				url: contextPath + '/www/media/' + item.id + '.json',
13625				loading: 'generic',
13626				confirmTitle: $translate.instant('msg.remove'),
13627				successMessage: $translate.instant('msg.removed')
13628			}).then(function() {
13629				refreshForm();
13630			});
13631		};
13632		
13633		function loadForm(item, data) {
13634			var url;
13635			if (data && data.id) {
13636				url = contextPath + '/www/geo/' + item.entity + '/' + data.id + '/edit.json';
13637			} else {
13638				url = contextPath + "/www/geo/" + item.entity + "/new.json";
13639				$scope.isNew = true;
13640			}
13641			if (item.type == 'formview') {
13642				url += "?form=" + item.formview;
13643			}
13644			$rest.get({
13645				skipHideLoading: true,
13646				url: url,
13647				loading: 'generic'
13648			}).then(function(o) {
13649
13650				$entity.getEntityForce(o.entity, function(meta) {
13651					if (meta.id === "user") {
13652						meta.columns = []; // FIXME user no columns???
13653					}
13654					item.meta = meta; // needed for translations
13655					item.model = o;//$utils.setModel(item.model, o);
13656					
13657					if (item.type == 'formview') {
13658						item.formviews = $utils.setFormviews(item.formviews, o);
13659						for (var k in item.formviews) {
13660							var fv = item.formviews[k];
13661							if (fv.form.id == item.formview) {
13662								item.form = fv;
13663							}
13664						}
13665					}
13666					
13667					// auto fill url params
13668					var search = $location.search();
13669					if (search) {
13670						for (var k in search) {
13671							var svalue = search[k];
13672							if (!$utils.isBlank(svalue)) {
13673								//console.log(k, svalue)
13674								var column = $entity.getColumn(item.entity, k);
13675								if (column != null) {
13676									//console.log(column);
13677									if (item.form != null) {
13678										item.form.data[k] = svalue;
13679									} else {
13680										item.model[k] = svalue;
13681									}
13682//									delete search[k];
13683//									$location.search(search).replace();
13684								}
13685							}
13686						}
13687					}
13688					
13689					// keep last scroll
13690					var scroll = $window.pageYOffset;
13691					$angular.apply($scope, function() {
13692						window.scrollTo(0, scroll);
13693					});					
13694				});
13695			});
13696		}
13697        
13698        function loadData(item) {
13699        	
13700        	if (item.type == 'form' || item.type == 'formview') {
13701    			loadForm(item);
13702        		return;
13703        	}
13704        	
13705        	if (item.action != 'entity' || $utils.isBlank(item.entity)) {
13706        		return;
13707        	}
13708
13709			$entity.getEntityForce(item.entity, function(meta) {
13710				//console.log(item.entity + " -> " + meta.id, meta)
13711				var limit = 2;
13712	        	if (item.type == 'preview' || item.type == 'preview-carousel') {
13713	        		if (item.preview == null) {
13714	        			item.preview = 4;
13715	        		} else {
13716	        			item.preview = Number(item.preview);
13717	        		}
13718	        		item.limit = item.preview + 1;
13719	        		limit = item.limit;
13720	        	}
13721	        	var url = contextPath + "/www/geo/" + item.entity + ".json?_limit=" + limit;
13722	        	if (item.instance) {
13723					url += "&instances=" + item.instance;
13724				}
13725				if (item.categories && item.categories.length > 0) {
13726					url += '&categories:in=' + $utils.join(item.categories,",");
13727				}
13728				if (item.query) {
13729					url += '&' + item.query;
13730				}
13731	        	item.entity = meta.id;
13732	        	$rest.get({
13733	    			url: url,
13734	    			force: true
13735	    		}).then(function(o) {
13736	    	    	item.data = o.list;
13737	    		});
13738			});
13739        	
13740        }
13741    	
13742    	$scope.hasAgenda = function() {
13743    		return !$utils.isBlank($sessionService.get("agenda_"+$instance.getId()));
13744    	};
13745
13746        $scope.hasText = function(item) {
13747        	return !$utils.isBlank($utils.parse(item.title))
13748        		|| !$utils.isBlank($utils.parse(item.description)); 
13749        };
13750        
13751        $scope.hasButtonText = function(item) {
13752        	return !$utils.isBlank($utils.parse(item.button));
13753        };
13754        
13755        $scope.hasAction = function(item) {
13756        	if ($utils.isBlank(item.action)) {
13757            	return false;
13758        	}
13759    		if (item.action == 'entity') {
13760    			if (!$utils.isBlank(item.entity)) {
13761    				if ($entity.isGeoForm(item.entity) && item.type != 'preview' && item.type != 'preview-carousel') {
13762    					return true;
13763    				}
13764    				if (item.type == 'preview' || item.type == 'preview-carousel') {
13765            			return item.data && item.data.length > item.preview;
13766    				}
13767        			return item.data && item.data.length > 0;
13768    			}
13769    			if ($utils.isBlank(item.instance)) {
13770    				return false;
13771    			}
13772    		}
13773    		if (item.action == 'link') {
13774    			return !$utils.isBlank(item.link);
13775    		}
13776    		return true;
13777        };
13778        
13779        $scope.getButtonText = function(item) {
13780        	var text = $utils.parse(item.button);
13781        	if (!$utils.isBlank(text)) {
13782        		return text;
13783        	}
13784        	if (item.action == 'entity') {
13785            	return $translate.instant("msg.web.seeAll");
13786        	}
13787        	return $translate.instant("msg.go");
13788        };
13789        
13790        // styles
13791        $scope.getStyleItem = function(item) {
13792        	if (!$utils.isBlank(item.background)) {
13793        		return { 'background-color':item.background }; 
13794        	}
13795        	return {}; 
13796        };
13797        $scope.getStyleItemImage = function(item) {
13798        	if (!$utils.isBlank(item.image)) {
13799        		return { 'background-image':'url('+$rootScope.$media.getMediaUrl(item.image, $angular.imgExtensionFinal)+')' }; 
13800        	}
13801        	return {}; 
13802        };
13803        $scope.getStyleItemText = function(item) {
13804        	if (item.type == 'default' || item.type == 'horizontal') {
13805        		if (!$utils.isBlank(item.image)) {
13806        			return { 'color':'var(--color-image-text)' };
13807        		}
13808        	}
13809        	if (!$utils.isBlank(item.background)) {
13810        		return { 'color':$utils.getColorText(item.background) }; 
13811        	}
13812        	return {}; 
13813        };
13814        $scope.getStyleImage = function(id) {
13815        	if ($instance.current) {
13816        		var images = $instance.current.geo_images;
13817        		if (images) {
13818        			var image = images[id];
13819        			if (image && image.width && image.height) {
13820        				var value = image.height * 100 / image.width;
13821        	        	return { 'height': value+'%' }; 
13822						//,'aspect-ratio': image.width + '/' + image.height
13823        			}
13824        		}
13825        	}
13826        	return {};
13827        };
13828    	
13829        // click items
13830        $scope.clickItem = function(e, item, index, carousel) {
13831        	$utils.preventDefault(e);
13832			
13833			var o = item;
13834			if (index != null && item.list != null) {
13835				item = item.list[index];
13836			}
13837        	//console.log(item)
13838        	
13839    		var url = null;
13840    		var params = {};
13841    		if (item.action == 'new_smart') {
13842    			url = $instance.getBaseUrl() + "plan";
13843    			
13844    		} else if (item.action == 'my_smart') {
13845    			if ($scope.hasAgenda()) {
13846        			url = $instance.getBaseUrl() + "agenda";
13847    			} else {
13848    				url = $instance.getBaseUrl() + "plan";
13849    			}
13850    			
13851    		} else if (item.action == 'link' && !$utils.isBlank(item.link)) {
13852    			url = item.link;
13853    		}
13854    		
13855    		if ($utils.isBlank(url)) {
13856    			if (item.entity) {
13857					if (item.data && item.data.length == 1) {
13858						url = $rootScope.getItemLinkBy(item.data[0]);
13859					} else if (item.entity) {
13860						url = $rootScope.getItemLink(item.entity, item.id);
13861					}
13862    				if (item.instance) {
13863    					params.instances = item.instance;
13864    				}
13865    				if (item.categories && item.categories.length > 0) {
13866    					params['categories:in'] = $utils.join(item.categories,",");
13867    				}
13868    				if (item.query) {
13869    					var o = queryStringToJSON(item.query);
13870    					for (var k in o) {
13871    						params[k] = o[k];
13872						}
13873    				}
13874    				
13875    			} else if (item.instance) {
13876    				url = "home/" + item.instance;
13877    			}
13878    		}
13879    		
13880    		if (!$utils.isBlank(url)) {
13881    			if (typeof url != 'string') {
13882    				$angular.log('url is not string: ' + url);
13883    				return;
13884    			}
13885    			if (!$utils.isTrue(item.new_tab)) {
13886    				if (url.contains('geodesignlabs.com/admin')) {
13887    					
13888    				} else if (url.contains('geodesignlabs.com/') && !url.contains('/media')) {
13889    					url = url.split('geodesignlabs.com/')[1];
13890    				}
13891    			}
13892    			if (url.startsWith('http')) {
13893    				if ($utils.isTrue(item.new_tab)) {
13894        				window.open(url);
13895    				} else {
13896    					window.location.href = url;
13897    				}
13898        			return;
13899    			}
13900    			if (url.startsWith('#')) {
13901    				var last = window.location.href;
13902    				if (last.contains("#")) {
13903    					last = last.split("#")[0];
13904    				}
13905    				window.location.href = last + url;
13906        			return;
13907    			}
13908				//console.log(url, params)
13909    			$location.path(url).search(params);
13910    			return;
13911    		}
13912
13913			item = o; // restore original
13914//			if (item.image) {
13915//				$rootScope.clickImage(null, [item], 0);
13916//				return;
13917//			}
13918    		if (item.type == 'gallery' && index != null) {
13919    			// open gallery in fullscreen
13920				$rootScope.clickImage(null, item.list, index);
13921            	return;
13922        	}
13923			
13924			if ((item.type == 'carousel' || item.type == 'preview-carousel') && carousel != null) {
13925				$rootScope.carouselNext(e, carousel);
13926            	return;
13927        	}
13928    		
13929        };
13930        
13931        initHome();
13932	}
13933
13934})();
13935
13936//// File web-list.js ////
13937(function() {
13938
13939	$angular.controllers['web-list'] = function($rootScope, $scope, $routeParam
13939s, $translate, $uibModal, $window, $document, $location) {
13940		$angular.preparePage({buttonsRight: "web-list-buttons.html"});
13941		$angular.contentScope = $scope;
13942		$angular.menuScope.selectedPage = "";
13943		$scope.showMap = false;
13944
13945		$scope.instanceId = $routeParams.instanceId;
13946		$scope.entity = $routeParams.entity;
13947		$scope.model = {};
13948		$angular.currentRoute += " " + $angular.currentRoute+"-"+$scope.entity;
13949
13950		var subInstance = $routeParams.instances;
13951		$scope.title = "";
13952		$scope.setTitle = function() {
13953			var title;
13954			if (!$utils.isBlank(subInstance)) {
13955				title = $instance.getSubInstanceName(subInstance);
13956			}
13957			if ($utils.isBlank(title)) {
13958				title = $instance.getEntityName($scope.entity);
13959			}
13960			$scope.title = title;
13961		};
13962		
13963		$entity.getEntityForce($scope.entity, function(o){
13964			$scope.meta = o;
13965			$scope.setTitle();
13966			$scope.showMap = $entity.isGeoLocation(o);
13967			
13968			if ($scope.showMap) {
13969				$angular.apply($scope, function() {
13970					initGeoIndexMap();
13971				});
13972			}
13973		});
13974
13975		$paginate.init($(".paginate"), {
13976			path: "www/geo/" + $scope.entity,
13977			noLoad: true
13978		});
13979
13980		function loadData(force, search) {
13981
13982			var qs = $paginator._settings.queryString;
13983			if (qs == null) qs = "";
13984			qs = removeParam(qs, "_index");
13985			qs = removeParam(qs, '_q');
13986
13987			if (!$utils.isBlank(search)) {
13988				search = search.replace("'", ""); // bug angularjs
13989				search = $utils.removeDiacritics(search.trim().toLowerCase());
13990			}
13991			if (!$utils.isBlank(search)) {
13992				// filter by search
13993				qs = addParam(qs, '_q', search);
13994			} else {
13995				var searchParams = $scope.searchParams;
13996				if (searchParams) {
13997					var text = searchParams.text;
13998					if (text) {
13999						text = text.replace("'", ""); // bug angularjs
14000						text = $utils.removeDiacritics(text.trim().toLowerCase());
14001						qs = addParam(qs, '_q', text);
14002					}
14003					var likes = searchParams.likes;
14004					Object.keys(likes).forEach(function(likeKey) {
14005						var paramKey = likeKey + ':like';
14006						qs = removeParam(qs, paramKey);
14007						var paramValue = likes[likeKey];
14008						if (paramValue && $scope.advancedSearch) {
14009							qs = addParam(qs, paramKey, paramValue);
14010						}
14011					});
14012					var options = searchParams.options;
14013					Object.keys(options).forEach(function(optionKey) {
14014						var paramKey = optionKey + ':in';
14015						qs = removeParam(qs, paramKey);
14016						var paramValue = options[optionKey];
14017						if (paramValue && $scope.advancedSearch) {
14018							qs = addParam(qs, paramKey, paramValue);
14019						}
14020					});
14021				}
14022			}
14023			$paginator.updateList(qs, force);
14024			refreshMarkers(true);
14025			window.scrollTo(0, 0);
14026
14027			$angular.apply($scope, function() {
14028				$angular.ready = true;
14029			});
14030		}
14031		
14032		// map
14033		$scope.showOrHideMap = function() {
14034			$scope.showMap = !$scope.showMap;
14035		};
14036
14037		// calendar
14038		$scope.showCalendar = false;
14039		$scope.showOrHideCalendar = function() {
14040			$scope.showCalendar = !$scope.showCalendar;
14041			if ($scope.showCalendar) {
14042				$scope.$broadcast('reloadCalendarComponent');
14043			}
14044		};
14045		
14046		$scope.calendarConfig = {
14047			initialView: 'dayGridMonth',
14048			getItems: function() {
14049				return $scope.model.list;
14050			},
14051			onClickEvent: function(event, item) {
14052				window.location.href = $rootScope.getItemLinkBy(item);
14053			},
14054			convertItemToEvent: function(item) {
14055				if (item.geo_schedule && item.geo_schedule.dates) {
14056					var timeZone = $utils.getTimeZone();
14057					var today = $utils.fromDate(new Date(), 'yyyy-MM-dd', 'UTC' + timeZone);
14058					var time = $utils.fromDate(new Date(), 'HH:mm', 'UTC' + timeZone);
14059					var oneYear = new Date();
14060					oneYear.setDate(oneYear.getDate() + 365);
14061					for (var d in item.geo_schedule.dates) {
14062						var date = item.geo_schedule.dates[d];
14063						if (date && date.day) {
14064							if (date.day > oneYear) {
14065								continue;
14066							}
14067							var endDay = date.end_day ? date.end_day : date.day;
14068							if (endDay < today) {
14069								continue;
14070							}
14071							if (endDay === today && date.time_to < time) {
14072								continue;
14073							}
14074
14075							var o = {
14076								id: item.id,
14077								start: date.day + 'T' + date.time_from + ':00',
14078								end: endDay + 'T' + date.time_to + ':00',
14079								title: $entity.displayColumn(item)
14080							};
14081							if (item.status) {
14082								o.classNames = ['fc-status-'+item.status];
14083							}
14084							return o;
14085						}
14086					}
14087				}
14088				return {};
14089			}
14090		};
14091
14092		var map;
14093		var infoWindow;
14094		var markers = {};
14095		var _tmp = 0;
14096
14097		function initGeoIndexMap() {
14098		
14099			markers = {};
14100			_tmp = 0;
14101			infoWindow = new google.maps.InfoWindow({maxWidth: 230, maxHeight: 230});
14102
14103			var mapOptions = {
14104				mapId : "GEO_LIST",
14105				streetViewControl: false, panControl: false,
14106				mapTypeId: google.maps.MapTypeId.ROADMAP,
14107				disableDefaultUI: false, zoomControl: true
14108			};
14109
14110			var gf = $instance.getGeoFence();
14111			if (gf && gf.center_latitude) {
14112				var lat = Number(gf.center_latitude);
14113				var lng = Number(gf.center_longitude);
14114				var pos = new google.maps.LatLng(lat, lng);
14115				var zoom = 12;
14116				if (gf.default_zoom) {
14117					zoom = Number(gf.default_zoom);
14118				}
14119				mapOptions.center = pos;
14120				mapOptions.zoom = zoom;
14121			} else {
14122				mapOptions.center = new google.maps.LatLng(0, 0);
14123				mapOptions.zoom = 3;
14124			}
14125			map = new google.maps.Map(document.getElementById('web-list-map'), mapOptions);
14126			//google.maps.event.addListenerOnce(map, 'idle', function(){});
14127			//google.maps.event.addListener(map, "click", function(event) {});
14128			//google.maps.event.addListener(map, "zoom_changed", function(event) {});
14129//			google.maps.event.addListener(map, "bounds_changed", function(event) {
14130//				_tmp++;
14131//				var val = _tmp;
14132//				setTimeout(function(){
14133//					if (val == _tmp) {
14134//						refreshMarkers();
14135//					}
14136//				}, 250);
14137//			});
14138
14139			google.maps.event.addListener(map, 'zoom_changed', function() {
14140				if (this.getZoom() > 17.5 && this.initialZoom) {
14141					this.setZoom(17.5);
14142					this.initialZoom = false;
14143				}
14144			});
14145
14146			refreshMarkers(true);
14147			$utils.prepareMapImages($scope.entity, map);
14148		}
14149
14150		function refreshMarkers(clean) {
14151
14152			if (clean == null) {
14153				clean = false;
14154			}
14155
14156			var qs = $paginator._settings.queryString;
14157			qs = removeParam(qs, "_order");
14158			qs = removeParam(qs, "_limit");
14159			qs = removeParam(qs, "_paginate");
14160			qs = addParam(qs, "_limit", 1000);
14161//			var b = map.getBounds();
14162//			if (b && b.ma && b.fa) {
14163//				qs = addParam(qs, "geo_location.latitude:$gt", b.ma.j);
14164//				qs = addParam(qs, "geo_location.longitude:$gt", b.fa.j);
14165//				qs = addParam(qs, "geo_location.latitude:$lt", b.ma.l);
14166//				qs = addParam(qs, "geo_location.longitude:$lt", b.fa.l);
14167//			}
14168			var url = contextPath + "/www/geo/" + $angular.contentScope.entity + "/a/map.json" + qs;
14169			$rest.get({
14170				//loading: "generic",
14171				url: url
14172			}).then(function(data) {
14173
14174				var bounds = new google.maps.LatLngBounds(null);
14175				var num = 0;
14176
14177				if (clean) {
14178					for (var id in markers) {
14179						var m = markers[id];
14180						m.setMap(null);
14181					}
14182					markers = {};
14183				}
14184
14185				for (var k in data.list) {
14186					var item = data.list[k];
14187					if (markers[item.id]) {
14188						continue; // exists
14189					}
14190					// add new
14191					var loc = item.geo_location;
14192					if (loc == null || loc.latitude == null || loc.longitude == null) {
14193						continue;
14194					}
14195					var pos = new google.maps.LatLng(loc.latitude, loc.longitude);
14196					var marker = new google.maps.Marker({
14197						position: pos,
14198						map: map, clickable: true
14199					});
14200					markers[item.id] = marker;
14201
14202					google.maps.event.addListener(marker, 'click', (function(plc, mark) {
14203						return function() {
14204							var href = $rootScope.getItemLinkBy(plc);
14205							var content = "<div class='infowindow'><a href='" + href + "' class='bold'>" + $entity.displayColumn(plc) + "</a><br>";
14206							content += "<label style='margin-top:5px; font-size: 14px;'>" + $utils.parse(plc.geo_location.address + "</label></div>");
14207							infoWindow.setContent(content);
14208							infoWindow.open(map, mark);
14209						};
14210					})(item, marker));
14211
14212					bounds.extend(pos);
14213					num++;
14214				}
14215
14216				if (num > 0 && map) {
14217					map.initialZoom = true;
14218					map.fitBounds(bounds);
14219				}
14220
14221			});
14222		}
14223
14224		$scope.searchParams = {
14225			text: $routeParams._q,
14226			likes: {},
14227			options: {}
14228		};
14229		$scope.advancedSearch = true; //false;
14230		$scope.searchOptions = {};
14231		$scope.searchOptionsSelected = {};
14232
14233		$scope.applySearch = function() {
14234			loadData(true);
14235		};
14236
14237		$scope.modifySearchColumnOption = function(column, itemId, $event) {
14238			if ($event) {
14239				$event.preventDefault();
14240			}
14241			var columnElement = $scope.searchOptionsSelected[column.name];
14242			if (!columnElement) {
14243				columnElement = {
14244					options: {},
14245					text: null
14246				};
14247				$scope.searchOptionsSelected[column.name] = columnElement;
14248			}
14249			var options = columnElement.options;
14250
14251			var option = options[itemId];
14252			if (!option) {
14253				option = {
14254					checked: false
14255				};
14256				options[itemId] = option;
14257			}
14258			option.checked = !option.checked;
14259
14260			var availableOptions = $scope.searchOptions[column.param];
14261			if (availableOptions == null) {
14262				availableOptions = [];
14263			}
14264			var checkedOptions = Object.keys(options)
14265				.filter(function(key) {
14266					return options[key].checked;
14267				});
14268			$scope.searchParams.options[column.name] = checkedOptions.join(',');
14269			columnElement.text = checkedOptions
14270				.map(function(key) {
14271					return availableOptions.filter(function(op) {
14272						return op.id === key;
14273					})[0] || null;
14274				})
14275				.filter(function(op) {
14276					return !!op;
14277				})
14278				.map(function(op) {
14279					return $entity.displayColumn(op);
14280				})
14281				.join(', ');
14282		};
14283
14284		function selectEntityFilterByParams(col) {
14285			var paramValue = $routeParams[col.name + ':in'];
14286			if (typeof paramValue == 'string') {
14287				paramValue.split(',').forEach(function(id) {
14288					$scope.modifySearchColumnOption(col, id);
14289				});
14290				$scope.advancedSearch = true;
14291			}
14292		}
14293
14294		var searchColumns = [];//$utils.cloneList($entity.getSearchColumns($scope.entity));
14295		var searchOptionsPromises = {};
14296		$entity.getSearchColumns($scope.entity).forEach(function(col) {
14297			
14298			if (col.isEntity) {
14299				searchColumns.push(col);
14300				if ($scope.searchOptions[col.param]) {
14301					selectEntityFilterByParams(col);
14302				} else {
14303					if (!searchOptionsPromises[col.param]) {
14304						var prom = $constants.getEntityList(col.param, null, null, null, 250);
14305						if (prom) {
14306							searchOptionsPromises[col.param] = prom.then(function(result) {
14307								$scope.searchOptions[col.param] = result;
14308							});
14309						}
14310					}
14311					if (searchOptionsPromises[col.param]) {
14312						searchOptionsPromises[col.param].then(function() {
14313							selectEntityFilterByParams(col);
14314						});
14315					}
14316				}
14317			} else {
14318				searchColumns.push(col);
14319				var paramValue = $routeParams[col.name + ':like'];
14320				if (paramValue) {
14321					$scope.searchParams.likes[col.name] = paramValue;
14322					$scope.advancedSearch = true;
14323				}
14324			}
14325		});
14326		var categoryList = $instance.getCategoryList($scope.entity);
14327		if (categoryList && categoryList.length > 0) {
14328			var categoriesColumn = {
14329				name: 'categories',
14330				isEntity: true,
14331				param: '__categories__'
14332			};
14333			searchColumns.push(categoriesColumn);
14334			$scope.searchOptions[categoriesColumn.param] = categoryList;
14335			selectEntityFilterByParams(categoriesColumn);
14336		}
14337		$scope.searchColumns = searchColumns;
14338		
14339		$scope.resetSearch = function() {
14340			
14341			// remove url params
14342			var qs = $paginator._settings.queryString;
14343			if (qs == null) qs = "";
14344			qs = removeParam(qs, "_index");
14345			qs = removeParam(qs, '_q');
14346			
14347			var searchParams = $scope.searchParams;
14348			var likes = searchParams.likes;
14349			Object.keys(likes).forEach(function(likeKey) {
14350				var paramKey = likeKey + ':like';
14351				qs = removeParam(qs, paramKey);
14352			});
14353			var options = searchParams.options;
14354			Object.keys(options).forEach(function(optionKey) {
14355				var paramKey = optionKey + ':in';
14356				qs = removeParam(qs, paramKey);
14357			});
14358
14359			// reset filter
14360			searchParams.text = '';
14361			searchParams.likes = {};
14362			searchParams.options = {};
14363			for (var k in $scope.searchOptionsSelected) {
14364				var columnElement = $scope.searchOptionsSelected[k];
14365				if (columnElement == null) {
14366					continue;
14367				}
14368				columnElement.options = {};
14369				columnElement.text = null;
14370			}
14371
14372			// update list
14373			console.log(qs)
14374			$paginator.updateList(qs, true);
14375			refreshMarkers(true);
14376			window.scrollTo(0, 0);
14377		};
14378
14379		loadData(true);
14380	}
14381
14382})();
14383
14384//// File web-pos.js ////
14385(function() {
14386
14387	$angular.controllers['web-pos'] = function($rootScope, $scope, $translate, $templateCache, $compile, $q) {
14388		$angular.preparePage();
14389		$angular.contentScope = $scope;
14390		$angular.menuScope.selectedPage = "";
14391		$rest.showNewVersion = true;
14392
14393		// only for admin/coordinators (new role vendedor?)
14394		$scope.config = $instance.current != null ? $instance.current.geo_pos : null;
14395		if ((!$user.isAdmin() && !$user.isCoordinator()) || $scope.config == null || !$scope.config.enabled) {
14396			$utils.showAccessDenied();
14397			return;
14398		}
14399		if ($scope.config.orders == null || $scope.config.products == null) {
14400			$utils.showAlert("Missing configuration");
14401			return;
14402		}
14403
14404		var isDev = location.host.indexOf('localhost') !== -1;
14405		if (!isDev) {
14406			// warn if back button
14407			window.onbeforeunload = function(e) { 
14408				return $translate.instant("msg.confirm.leave"); 
14409			};
14410		}
14411		
14412		$scope.payTypeList = [];
14413		$entity.getEntityForce($scope.config.orders, function(o){
14414			$scope.meta = o;
14415			//console.log('meta', o)
14416			o.columns.forEach(function(column) {
14417				if (column.name == 'pay_type') {
14418					$scope.payTypeList = column.options;
14419					return;
14420				}
14421			});
14422		});
14423
14424		$scope.filter = {
14425			orders: $storageService.get("_pos_orders"),
14426			search1: null,
14427			search2: null
14428		};
14429		
14430		$scope.platforms = [
14431			{ id: 'local', name: 'Local', icon: 'store', priceField: null } // required
14432			//,{ id: 'uber',  name: 'Uber',  icon: 'directions_car', priceField: 'price_uber' }
14433			//,{ id: 'rappi', name: 'Rappi', icon: 'moped',          priceField: 'price_rappi' }
14434		];
14435		
14436		$scope.initOrder = function(order) {
14437			parseClients(order);
14438		};
14439		$scope.showOrders = function(type) {
14440			if (type) {
14441				$scope.filter.orders = type;
14442			}
14443			$scope.showList = true;
14444			$angular.title = $translate.instant("msg.order.list");
14445			$scope.order = null;
14446			$scope.getOrders();
14447		};
14448		$scope.newOrder = function() {
14449			$scope.showList = false;
14450			$angular.title = $translate.instant("msg.order.new");
14451
14452			$scope.order = {
14453				entity: $scope.config.orders,
14454				items: [],
14455				platform: $scope.platforms[0] // Default a local
14456			};
14457			refreshTotals(false);
14458			$scope.original = $utils.clone($scope.order);
14459			
14460			// refresh products for availability
14461			$scope.getProducts();
14462		};
14463		$scope.showOrder = function(item) {
14464			//console.dir(item)
14465			$scope.showList = false;
14466			$angular.title = $translate.instant("msg.order") + " #" + item.code;
14467			
14468			item.clients = null;
14469			$scope.order = item;
14470			// Asegurar que exista una plataforma asignada en órdenes previas
14471			if (!$scope.order.platform) {
14472				$scope.order.platform = $scope.platforms[0];
14473			}
14474			
14475			refreshTotals(false);
14476			$scope.original = $utils.clone($scope.order);
14477			
14478			// refresh products for availability
14479			$scope.getProducts();
14480		};
14481		
14482		
14483		var flag = 0;
14484		var updating = null;
14485		$scope.update = function(order) {
14486			flag++;
14487			
14488			var tmp = order;
14489			if (tmp == null) {
14490				tmp = $scope.order;
14491			}
14492
14493			if (updating != null && updating != tmp) {
14494				updateOrder(updating);
14495				updating = null;
14496			}
14497			
14498			if (flag > 100 || order != null) {
14499				flag = 0;
14500				updating = null;
14501				updateOrder(tmp);
14502				return;
14503			}
14504			
14505			var flag2 = flag;
14506			updating = tmp;
14507			setTimeout(function() {
14508				if (flag2 == flag && updating == tmp) {
14509					flag = 0;
14510					updating = null;
14511					updateOrder(tmp);
14512				}
14513			}, 750);
14514		
14515		};
14516
14517		function updateOrder(tmp) {
14518		
14519			var data = $utils.clone(tmp);
14520			delete data.clients;
14521			
14522			var loading = (data.id == null || (data.paid != null  && $scope.original != null && $scope.original.paid == null));
14523			$rest.post({
14524				url: contextPath + "/api/pos.json",
14525				data: data,
14526				loading: loading ? 'generic' : null
14527			}).then(function(o) {
14528				if (tmp == $scope.order) {
14529					if (tmp.id == null) {
14530						// created
14531						$scope.showOrder(o);
14532					} else {
14533						// updated - save last status
14534						$scope.original = $utils.clone($scope.order);
14535			
14536						// refresh products for availability
14537						$scope.getProducts();
14538					}
14539				} else {
14540					// refresh list
14541					$scope.getOrders();
14542				}
14543				
14544			}).catch(function() {
14545				if (tmp == $scope.order) {
14546					// restore original object
14547					$scope.order = $scope.original;
14548				} else {
14549					// refresh list
14550					$scope.getOrders();
14551				}
14552			});
14553
14554		}
14555		$scope.remove = function() {
14556			if ($scope.order.id == null) {
14557				$scope.showOrders();
14558				return;
14559			}
14560			$rest.remove({
14561				confirmTitle: $translate.instant("msg.remove") + " #" + $scope.order.code,
14562				url: contextPath + "/api/pos/" + $scope.order.id + ".json?force=true",
14563				//successMessage: $translate.instant("msg.removed")
14564			}).then(function(o) {
14565				//console.log('remove', o)
14566				$scope.showOrders();
14567			});
14568		}
14569
14570		$scope.getOrders = function() {
14571
14572			if ($utils.isBlank($scope.filter.orders)) {
14573				$scope.filter.orders = 'all';
14574			}
14575
14576			var url = contextPath + "/api/pos.json?no-projection=true&_limit=200";
14577			if ($scope.filter.orders == 'to_pay') {
14578				url += "&paid:$exists=false";
14579			} else if ($scope.filter.orders == 'to_deliver') {
14580				url += "&paid:$exists=true&delivered:$exists=false";
14581			} else {
14582				// all
14583			}
14584			$storageService.put("_pos_orders", $scope.filter.orders);
14585			
14586			var search = $scope.filter.search1;
14587			if (!$utils.isBlank(search)) {
14588				search = search.replace("'", ""); // bug angularjs
14589				search = $utils.removeDiacritics(search.trim().toLowerCase());
14590			}
14591			if (!$utils.isBlank(search)) {
14592				url += "&_q=" + search;
14593			}
14594
14595			$rest.get({
14596				url: url
14597			}).then(function(o) {
14598				$scope.orders = o.list;
14599			});
14600		}
14601		
14602		$scope.getCategories = function() {
14603			$rest.get({
14604				url: contextPath + "/api/pos/a/categories.json"
14605			}).then(function(o) {
14606				$scope.categories = o.list;
14607			});
14608		}
14609		
14610		var productsMap = {};
14611		var productsUrl = null;
14612		$scope.getProducts = function(callback) {
14613
14614			var url = contextPath + "/api/pos/a/products.json?_limit=100";
14615			if ($scope.filter.category != null) {
14616				url += "&categories=" + $scope.filter.category.id + "&";
14617			}
14618			var search = $scope.filter.search2;
14619			if (!$utils.isBlank(search)) {
14620				search = search.replace("'", ""); // bug angularjs
14621				search = $utils.removeDiacritics(search.trim().toLowerCase());
14622			}
14623			if (!$utils.isBlank(search)) {
14624				url += "&_q=" + search;
14625			}
14626			
14627			if (url == productsUrl && callback == null) {
14628				// skip request
14629				productsUrl = null;
14630				return;
14631			}
14632			productsUrl = url;
14633
14634			$rest.get({
14635				url: url,
14636				loading: (callback != null) ? 'generic' : null
14637			}).then(function(o) {
14638				$scope.products = o.list;
14639				
14640				// save products in cache
14641				o.list.forEach(function(item) {
14642					productsMap[item.id] = item;
14643					
14644					var stock = getStock(item);
14645					if (stock > 0 && stock < 3) {
14646						productsUrl = null; // omit skip request
14647					}
14648				});
14649				
14650				if (callback) {
14651					callback();
14652				}
14653			});
14654		};
14655
14656		// products search
14657		$scope.clickCategory = function(category) {
14658			$scope.filter.category = category;
14659			$scope.getProducts();
14660		}
14661		$scope.cleanSearch1 = function() {
14662			$scope.filter.search1 = "";
14663			$scope.getOrders();
14664		}
14665		$scope.cleanSearch2 = function() {
14666			$scope.filter.search2 = "";
14667			$scope.getProducts();
14668		}
14669
14670		$scope.addItem = function(product) {
14671			if (!$scope.hasStock(product)) {
14672				return;
14673			}
14674			for (var k in $scope.items) {
14675				var item = $scope.items[k];
14676				if (item.product && item.product.entity_id == product.id) {
14677					$scope.addQuantity(null, item);
14678					return;
14679				}
14680			}
14681			
14682			var priceField = $scope.order.platform && $scope.order.platform.priceField ? $scope.order.platform.priceField : null;
14683			var price = priceField && product[priceField] != null ? product[priceField] : product.geo_buy.price;
14684
14685			$scope.items.push({
14686				id: '' + Date.now(), // for inner entity list
14687				name: $entity.displayColumn(product),
14688				quantity: 1,
14689				price: price,
14690				tax: product.geo_buy.tax, // TODO
14691				amount: price,
14692				product: {
14693					entity: product.entity,
14694					entity_id: product.id
14695				}
14696			});
14697			
14698			$angular.apply($scope, function() {
14699				if ($scope.items == $scope.order.clients[$scope.order.clients.length-1]) {
14700					// is last scroll down
14701					var objDiv = document.getElementById("preorder-body");
14702					if (objDiv != null) {
14703						objDiv.scrollTop = objDiv.scrollHeight;
14704					}
14705				}
14706				refreshTotals(true);
14707			});
14708			
14709		};
14710		$scope.removeItem = function(list, item) {
14711			$utils.removeFromArray(list, item);
14712			if (list.length == 0 && $scope.order.clients.length > 1) {
14713				$utils.removeFromArray($scope.order.clients, list);
14714				$scope.items = $scope.order.clients[0];
14715			}
14716			refreshTotals(true);
14717		};
14718		
14719		$scope.selectClient = function(client) {
14720			$scope.items = client;
14721		};
14722
14723		$scope.setPlatform = function(platform) {
14724			if ($scope.order.platform == platform) return;
14725			$scope.order.platform = platform;
14726			
14727			// update prices of existing items
14728			var priceField = platform.priceField;
14729			for (var k in $scope.order.items) {
14730				var item = $scope.order.items[k];
14731				if (item.product) {
14732					var product = productsMap[item.product.entity_id];
14733					if (product) {
14734						var newPrice = priceField && product[priceField] != null ? product[priceField] : product.geo_buy.price;
14735						item.price = newPrice;
14736					}
14737				}
14738			}
14739			refreshTotals(true);
14740		};
14741		
14742		$scope.addClient = function() {
14743			var list = $scope.order.clients;
14744			if (list.length > 0 && list[list.length - 1].length == 0) {
14745				return;
14746			}
14747			$scope.items = [];
14748			$scope.order.clients.push($scope.items);
14749		};
14750		$scope.addDiscount = function() {
14751			$angular.openModal("panel-form", {
14752				title: $translate.instant('msg.discount'),
14753				items: [
14754					{ type: 'number', name: 'discount', label: $translate.instant('msg.discount') }
14755				],
14756				successFunction: function(o) {
14757					if (o.discount > 0) {
14758						$scope.items.push({
14759							id: '' + Date.now(), // for inner entity list
14760							discount: o.discount
14761						});
14762						refreshTotals(true);
14763					}
14764					return null;
14765				}
14766			});
14767		};
14768		$scope.addNote = function() {
14769			$angular.openModal("panel-form", {
14770				title: $translate.instant('msg.note'),
14771				items: [
14772					{ type: 'text', name: 'note', label: $translate.instant('msg.note') }
14773				],
14774				successFunction: function(o) {
14775					if (!$utils.isBlank(o.note)) {
14776						$scope.items.push({
14777							id: '' + Date.now(), // for inner entity list
14778							note: o.note
14779						});
14780						refreshTotals(true);
14781					}
14782					return null;
14783				}
14784			});
14785		};
14786		
14787		$scope.hasMoreStock = function(item) {
14788			if (item.product) {
14789				var product = productsMap[item.product.entity_id];
14790				if (product) {
14791					return $scope.hasStock(product);
14792				}
14793			}
14794			return true;
14795		};
14796
14797		$scope.addQuantity = function(e, item) {
14798			$utils.preventDefault(e);
14799			
14800			if (!$scope.hasMoreStock(item)) {
14801				return;
14802			}
14803			
14804			if (item.quantity == null) item.quantity = 0;
14805			item.quantity++;
14806			refreshTotals(true);
14807		};
14808
14809		$scope.removeQuantity = function(e, list, item) {
14810			$utils.preventDefault(e);
14811			
14812			if (item.quantity == null || item.quantity === 1) {
14813				$scope.removeItem(list, item);
14814				return;
14815			}
14816			item.quantity--;
14817			refreshTotals(true);
14818		};
14819		
14820		$scope.refreshQuantity = function() {
14821			refreshTotals(true);
14822		};
14823		
14824		function parseClients(order) {
14825			var clients = [[]];
14826			for (var k in order.items) {
14827				var item = order.items[k];
14828				if (item.client == null || item.client == 0) {
14829					item.client = 1;
14830				}
14831				var list = clients[item.client-1];
14832				if (list == null) {
14833					list = [];
14834					clients.push(list);
14835				}
14836				list.push(item);
14837			}
14838			order.clients = clients;
14839		}
14840
14841		function refreshTotals(update) {
14842			
14843			if ($scope.order.clients == null) {
14844				// parse items to clients
14845				parseClients($scope.order);
14846			}
14847			
14848			if (!update) {
14849				$scope.items = $scope.order.clients[0];
14850			}
14851			
14852			// parse clients to items
14853			var items = [];
14854			$scope.order.clients.forEach(function(list, index) {
14855				list.forEach(function(item) {
14856					item.client = index + 1;
14857					items.push(item);
14858				});
14859			});
14860			$scope.order.items = items;
14861			
14862			// parse total
14863			var total = 0;
14864			var tax = 0;
14865			for (var k in $scope.order.items) {
14866				var item = $scope.order.items[k];
14867				if (item.note != null) {
14868					continue;
14869				}
14870				if (item.price) {
14871					var amount = item.price * item.quantity;
14872					item.amount = amount;
14873					total += amount;
14874				}
14875				if (item.discount != null) {
14876					item.amount = -item.discount;
14877					total -= item.discount;
14878				}
14879				if (item.tax) {
14880					tax += (item.tax * item.quantity);
14881				}
14882			}
14883			$scope.order.base = total;
14884			$scope.order.tax = tax;
14885			$scope.order.amount = total + tax;
14886
14887			if (update) {
14888				$scope.update();
14889			}
14890		}
14891
14892		$scope.showStatus = function(order) {
14893			var list = [];
14894			if (order.paid) {
14895				if (order.payment) {
14896					list.push($translate.instant("msg.paid.web"));
14897				}
14898				if (!$utils.isBlank(order.pay_type)) {
14899					list.push($utils.toText(order.pay_type));
14900				}
14901			}
14902			if (order.delivery_method != null && !$utils.isBlank(order.delivery_method.type)) {
14903				list.push(order.delivery_method.type);
14904			}
14905			if (order.delivered) {
14906				list.push("<b>" + $translate.instant("msg.delivered") + "</b>");
14907			}
14908			return $utils.join(list, "<br>");
14909		}
14910		
14911		$scope.hasStock = function(item) {
14912			return getStock(item) > 0;
14913		};
14914		function getStock(item) {
14915			return item.geo_buy != null ? item.geo_buy.stock : 0;
14916		}
14917
14918		$scope.checkout = function() {
14919			
14920			$scope.getProducts(function() {
14921				
14922				if ($scope.order == null) {
14923					return;
14924				}
14925				
14926				// validate stock
14927				for (var k in $scope.order.items) {
14928					var item = $scope.order.items[k];
14929					if (item.product) {
14930						var product = productsMap[item.product.entity_id];
14931						if (getStock(product) < 0) {
14932							$utils.floatingError("No hay suficiente stock de " + $entity.displayColumnHtml(product)); // TODO translate
14933							return;
14934						}
14935					}
14936				}				
14937				
14938				if ($scope.payTypeList.length > 0) {
14939					$scope.order.pay_type = $scope.payTypeList[0];
14940				}
14941				$scope.order.paid = new Date().getTime();
14942				
14943				$scope.update();
14944			});
14945			
14946			
14947		};
14948		$scope.payType = function(e, type) {
14949			$utils.preventDefault(e);
14950			
14951			if ($scope.order.pay_type == type) {
14952				$scope.order.pay_type = null;
14953				$scope.order.paid = null;
14954			} else {
14955				$scope.order.pay_type = type;
14956			} 
14957			
14958			$scope.update();
14959		};
14960		$scope.deliver = function(e, order) {
14961			$utils.preventDefault(e);
14962
14963			if (order.delivered) {
14964				order.delivered = null;
14965			} else {
14966				order.delivered = new Date().getTime();
14967			}
14968			
14969			$scope.update(order);
14970		};
14971		
14972		$scope.print = function() {
14973			var template = $templateCache.get("web-pos-print.html");
14974			var now = new Date().getTime();
14975			var printScope = angular.extend($rootScope.$new(), { order:$scope.order, now:now });
14976			var element = $compile($('<div>' + template + '</div>'))(printScope);
14977			$angular.apply(printScope, function() {
14978				try {
14979					var html = element.html();
14980					$utils.printHtml(html, false);
14981					printScope.$destroy(); // To avoid memory leaks from scope create by $rootScope.$new()
14982				} catch (e) {
14983					$init.logError(e);
14984				}
14985			})
14986		};
14987
14988		// init
14989		$scope.showOrders();
14990		$scope.getCategories();
14991
14992	}
14993
14994})();
14995
14996//// File web-orders.js ////
14997(function() {
14998
14999	$angular.controllers['web-orders'] = function($rootScope, $scope, $translate) {
15000		$angular.preparePage();
15001		$angular.contentScope = $scope;
15002
15003		$scope.title = "";
15004		$scope.setTitle = function() {
15005			$scope.title = $translate.instant("msg.orders");
15006		};
15007		$scope.setTitle();
15008
15009		$scope.model = {
15010			orders: []
15011		};
15012		$rest.get({
15013			skipHideLoading: true,
15014			loading: "generic",
15015			url: contextPath + "/www/orders.json"
15016		}).then(function(o) {
15017			$scope.model.orders = o.orders;
15018
15019			$angular.apply($scope, function() {
15020				$angular.ready = true;
15021			});
15022		});
15023	}
15024})();
15025
15026//// File web-bookings.js ////
15027(function() {
15028
15029	$angular.controllers['web-bookings'] = function($rootScope, $scope, $translate) {
15030		$angular.preparePage();
15031		$angular.contentScope = $scope;
15032
15033		$scope.title = "";
15034		$scope.setTitle = function() {
15035			$scope.title = $translate.instant("msg.bookings");
15036		};
15037		$scope.setTitle();
15038
15039		$scope.model = {
15040			items: []
15041		};
15042		$rest.get({
15043			skipHideLoading: true,
15044			loading: "generic",
15045			url: contextPath + "/www/booking.json"
15046		}).then(function(o) {
15047			$scope.model.items = o.items;
15048
15049			$angular.apply($scope, function() {
15050				$angular.ready = true;
15051			});
15052		});
15053	}
15054})();
15055
15056//// File web-select.js ////
15057(function() {
15058    
15059    
15060    $angular.controllers['web-select'] = function($rootScope, $scope, $routeParams, $translate, $window, $document, $location) {
15061		$angular.preparePage({ resetBacks:true, buttonsRight:"web-select-buttons.html" });
15062    	$angular.contentScope = $scope;
15063
15064		window.location.href = contextPath + "/admin"
15065    	
15066		/*
15067    	if (!$user.isSuper()) {
15068    		window.location.href = contextPath + "/admin"
15069    		return;
15070    	}
15071    	
15072		$scope.withDomains = true;
15073		$scope.toggleWithDomains = function() {
15074			$scope.withDomains = !$scope.withDomains;
15075			$scope.refresh();
15076		}
15077		
15078    	$scope.list = [];
15079		$scope.refresh = function() {
15080			
15081			var url = contextPath + "/www/geo/instance.json?_paginate=false";
15082			if ($scope.withDomains) {
15083				url += "&has-domain=true";
15084			}
15085			
15086			$rest.get({
15087				skipHideLoading: true,
15088				loading: "generic",
15089				url: url
15090			}).then(function(o) {
15091				
15092				var isDev = location.host.indexOf('localhost') !== -1;
15093
15094				for (var k in o.list) {
15095					var item = o.list[k];
15096					if (!isDev && $scope.withDomains && item.domains && item.domains.length > 0) {
15097						item.url = "https://" + item.domains[0];
15098						continue;
15099					}
15100					item.url = "home/" + item.id;
15101				}
15102				
15103				$scope.list = o.list;
15104			});
15105		};
15106		$scope.refresh();
15107		*/
15108
15109	}
15110	
15111})();
15112
15113//// File web-order.js ////
15114(function() {
15115
15116	$angular.controllers['web-order'] = function($rootScope, $scope, $routeParams, $translate, $uibModal, $window, $document, $location) {
15117		$angular.preparePage({buttonsRight: "web-order-buttons.html"});
15118		$angular.contentScope = $scope;
15119
15120		$scope.title = "";
15121		$scope.setTitle = function() {
15122			$scope.title = $translate.instant("msg.order");
15123		};
15124		$scope.setTitle();
15125
15126		$scope.showIsNewMessage = $location.search().isNew;
15127		$location.search('isNew', null).replace();
15128		$scope.closeIsNewMessage = function() {
15129			$scope.showIsNewMessage = false;
15130		};
15131
15132		var pathToOrder = "/www/orders/" + $routeParams.id + ".json";
15133		if (!$user.isLogged()) {
15134			var code = $location.search().code;
15135			if (code) {
15136				pathToOrder = "/www/orders-public/" + $routeParams.id + ".json?code=" + code;
15137			} else {
15138				$angular.goLogin();
15139				return;
15140			}
15141		}
15142		$rest.get({
15143			skipHideLoading: true,
15144			loading: "generic",
15145			url: contextPath + pathToOrder
15146		}).then(function(order) {
15147			$scope.model = order;
15148
15149			var deliveryMethod = order.delivery_method;
15150			if (deliveryMethod && deliveryMethod.extra_data && deliveryMethod.extra_data_entity) {
15151				$scope.deliveryMethodFormEntity = $entity.getEntity(deliveryMethod.extra_data_entity);
15152			}
15153
15154			$angular.apply($scope, function() {
15155				$angular.ready = true;
15156			});
15157		});
15158
15159		$scope.clickItem = function($event, item) {
15160			$utils.preventDefault($event);
15161			var url = $angular.rootScope.getItemLinkBy(item.object);
15162			$location.path(url);
15163		};
15164	}
15165
15166})();
15167
15168//// File web-item.js ////
15169(function() {
15170
15171	$angular.controllers['web-item'] = function($rootScope, $scope, $sce, $routeParams, $translate, $location, $window, NgMap, geolocationService) {
15172		$angular.preparePage({title: '', buttonsRight: "web-item-buttons.html"});
15173		$angular.contentScope = $scope;
15174		$angular.menuScope.selectedPage = "";
15175		$scope.entity = $routeParams.entity;
15176		$scope.entityId = $routeParams.entityId;
15177		$angular.currentRoute += " " + $angular.currentRoute+"-"+$scope.entity;
15178		
15179		if ($entity.isGeoUser($scope.entity) && !$user.isLogged()) {
15180			$angular.tmpPath = $location.path();
15181			$angular.goLogin();
15182			return;
15183		}
15184		
15185		if ($scope.entityId == "new") {
15186			$scope.entityId = null;
15187			$scope.isNew = true;
15188		}
15189		
15190		$scope.meta = null;
15191		$scope.model = null;
15192		$scope.isItem = false;
15193		$scope.isForm = false;
15194		$scope.isFormview = false;
15195		
15196		// get custom formview id
15197		var search = $location.search();
15198		if (search && search.form) {
15199			$scope.formId = search.form;
15200		}
15201		
15202		// load entity meta
15203		$entity.getEntityForce($scope.entity, function(o){
15204			$scope.meta = o;
15205			//console.log("meta", $scope.meta)
15206			
15207			$scope.haveDocuments = $entity.isAttachDocuments($scope.meta);
15208			if (!$scope.haveDocuments) {
15209				if ($scope.meta && $scope.meta.columns) {
15210					for (var k in $scope.meta.columns) {
15211						var column = $scope.meta.columns[k];
15212						if (column.type == 'document') {
15213							$scope.haveDocuments = true;
15214							break;
15215						}
15216					}
15217				}
15218			}
15219		
15220			if ($entity.isAutoLocation($scope.entity)) {
15221				geolocationService.getCurrentLocation().then(function(location) {
15222					$scope.currentGeoLocation = location;
15223				});
15224			}
15225			
15226			refresh();
15227		});
15228		
15229		$scope.refresh = function() {
15230			refresh();
15231		};
15232		function refresh(callback) {
15233			var url;
15234			if ($utils.isBlank($scope.entityId)) {
15235				url = contextPath + "/www/geo/" + $scope.entity + "/new.json";
15236			} else {
15237				url = contextPath + '/www/geo/' + $scope.entity + '/' + $scope.entityId + '/edit.json';
15238			}
15239			if ($scope.formId != null) {
15240				url += "?form=" + $scope.formId;
15241			}
15242			$rest.get({
15243				skipHideLoading: true,
15244				url: url,
15245				loading: 'generic'
15246			}).then(function(o) {
15247				$scope.model = o;
15248				//console.log("model", $scope.model)
15249				
15250				if (!$utils.isBlank($scope.entityId) && $scope.entityId != o.id
15250) {
15251					$scope.entity = o.entity;
15252					$scope.entityId = o.id;
15253					//console.log('updated entity/id ' + o.entity + " " + o.id)
15254				}
15255				
15256				$scope.formviews = $utils.setFormviews($scope.formviews, o);
15257				if ($scope.formviews.length > 0) {
15258					for (var k in $scope.formviews) {
15259						var fv = $scope.formviews[k];
15260						if ($scope.form == null || fv.form.id == $scope.formId) {
15261							$scope.form = fv;
15262						}
15263					}
15264					//console.log($scope.form)
15265					$scope.isFormview = true;
15266					$scope.opts = {};
15267					$scope.opts.readonly = $scope.form.form.readonly;
15268
15269				} else if ($entity.isGeoForm($scope.entity)) {
15270					$scope.isForm = true;
15271					
15272				} else {
15273					$scope.isItem = true;
15274					
15275					if (o.geo_media == null) o.geo_media = {};
15276					if (o.geo_media.images == null) o.geo_media.images = [];
15277					if (o.geo_location == null) o.geo_location = {};
15278					parseData(o);
15279					$scope.hasShare = navigator.share;
15280				}
15281				
15282				// auto fill url params
15283				if ($scope.isForm || $scope.isFormview) {
15284					var search = $location.search();
15285					if (search) {
15286						for (var k in search) {
15287							var item = search[k];
15288							//console.log(k, item)
15289							if (!$utils.isBlank(item)) {
15290								if ($entity.getColumn($scope.entity, k) != null) {
15291									if ($scope.form != null) {
15292										$scope.form.data[k] = item;
15293									} else {
15294										$scope.model[k] = item;
15295									}
15296//									delete search[k];
15297//									$location.search(search).replace();
15298								}
15299							}
15300						}
15301					}	
15302	
15303					NgMap.getMap().then(function (map) { // TODO only return first map - use getMap(id)
15304						if (map.getZoom() > 15) {
15305							map.setZoom(15);
15306						}
15307					});
15308				}
15309
15310				$angular.apply($scope, function() {
15311					$angular.ready = true;
15312				});
15313				
15314				if (callback) {
15315					callback();
15316				}
15317			});
15318		}
15319		
15320		var webForm;
15321    	var webFormCallback;
15322    	$scope.initForm = function(form) {
15323    		webForm = form;
15324    	}
15325    	$scope.saveForm = function(callback) {
15326    		webFormCallback = callback;
15327    		setTimeout(function(){
15328    			$("#webForm")[0].requestSubmit(); // to validate form
15329			}, 100);
15330    	}
15331    	$scope.submitForm = function(e) {
15332    		$utils.preventDefault(e);
15333    		
15334			if (!$utils.validateForm(webForm.webForm)) {
15335				return;
15336			}
15337			
15338			var model = $scope.model;
15339			var isCreate = $scope.model.id == null;
15340			
15341			if ($scope.isFormview) {
15342				isCreate = $scope.form.data.id == null;
15343	        	model = {
15344					id: $scope.form.data.id,
15345					formId: $scope.form.form.id,
15346					data: $scope.form.data
15347				};
15348			}
15349
15350			if ($scope.currentGeoLocation) {
15351				model.geo_location = {
15352					latitude: $scope.currentGeoLocation.latitude,
15353					longitude: $scope.currentGeoLocation.longitude
15354				};
15355			}
15356			
15357			$rest.post({
15358				url: contextPath + "/www/geo/" + $scope.entity + ".json",
15359				data: model,
15360				loading: "generic"
15361			}).then(function(data) {
15362			
15363				if (isCreate) {
15364					$scope.entityId = data.id;
15365					if ($scope.isFormview) {
15366						$scope.form.data.id = data.id;
15367					} else {
15368						$scope.model.id = data.id;
15369					}
15370				}
15371				
15372				$utils.postSave(model, data, isCreate, webFormCallback, refresh);
15373			});
15374		};
15375		
15376		$scope.resetForm = function() {
15377			$scope.entityId = null;
15378			refresh();
15379		};
15380		
15381		// attach documents
15382		$scope.openUploadFile = function(e) {
15383			$utils.preventDefault(e);
15384			$('#webFormAttachments input[type="file"]').trigger('click');
15385		};
15386
15387		$scope.uploadFile = function(files) {
15388			if (files.length === 0) {
15389				return;
15390			}
15391			var file = files[0];
15392			var reader = new FileReader();
15393			reader.onload = function(event) {
15394				$rest.post({
15395					url: contextPath + '/www/media/a/createBase64.json',
15396					data: {
15397						fileBase64: event.target.result.split(',')[1],
15398						fileName: file.name,
15399						entity: $scope.entity,
15400						entityId: $scope.entityId
15401					},
15402					loading: 'generic',
15403					successMessage: $translate.instant('msg.saved')
15404				}).then(function() {
15405					refresh();
15406				});
15407			};
15408			reader.readAsDataURL(file);
15409		};
15410		
15411		$scope.removeFile = function(item) {
15412			$rest.remove({
15413				url: contextPath + '/www/media/' + item.id + '.json',
15414				loading: 'generic',
15415				confirmTitle: $translate.instant('msg.remove'),
15416				successMessage: $translate.instant('msg.removed')
15417			}).then(function() {
15418				refresh();
15419			});
15420		};
15421		
15422		
15423		// old web-item 		
15424		var hasYoutube = false;
15425		var hasMedia = false;
15426		var data1 = [];
15427		var dataRight = [];
15428
15429		function parseData(model) {
15430
15431			hasMedia = false;
15432			hasYoutube = false;
15433			data1 = [];
15434			dataRight = [];
15435
15436			// buy
15437			if ($cart.isCartItem(model)) {
15438				addItem({
15439					force: true,
15440					type: 'buy',
15441					stock: $cart.getStock(model)
15442				});
15443			}
15444
15445			if ($entity.isGeoLocation($scope.entity)) {
15446				
15447				if (model.geo_location == null) {
15448					model.geo_location = {};
15449				}
15450
15451				// address
15452				if (model.geo_location.address) {
15453					addItem({
15454						type: 'text',
15455						name: 'address',
15456						label: $translate.instant("msg.address"),
15457						value: $utils.parse(model.geo_location.address),
15458						from: 'geo_location'
15459					});
15460				}
15461
15462				// map
15463				var lat;
15464				var lng;
15465				var zoom;
15466				var label;
15467				if (model.geo_fence) {
15468					lat = model.geo_fence.center_latitude;
15469					lng = model.geo_fence.center_longitude;
15470					zoom = model.geo_fence.default_zoom;
15471				}
15472				if (!lat || !lng) {
15473					lat = model.geo_location.latitude;
15474					lng = model.geo_location.longitude;
15475				}
15476				if (model.geo_location.address == null) {
15477					label = $translate.instant("msg.address");
15478				}
15479				if (lat && lng) {
15480					addItem({
15481						force: true,
15482						label: label,
15483						type: 'map',
15484						latitude: lat,
15485						longitude: lng,
15486						zoom: zoom ? zoom : 15
15487					});
15488				}
15489			}
15490
15491			// timetables
15492			if ($entity.isGeoLocation($scope.entity) && !$utils.isBlank(model.geo_location.timetables)) {
15493				addItem({
15494					type: 'html',
15495					name: 'timetables',
15496					label: $utils.parseGeoTranslation('timetables'),
15497					value: getTimeTables(model.geo_location.timetables)
15498				});
15499			}
15500
15501			// schedule
15502			if ($entity.isGeoSchedule($scope.entity) && model.geo_schedule) {
15503				var geoScheduleBooking = model.geo_schedule.booking;
15504				if (geoScheduleBooking && geoScheduleBooking.enabled) {
15505					addItem({
15506						type: 'booking',
15507						force: true,
15508						bookableObject: model
15509					});
15510				} else {
15511					var value = $utils.printScheduledDates(model, 10);
15512					if (!$utils.isBlank(value)) {
15513						addItem({
15514							type: 'html',
15515							name: 'schedule',
15516							label: $utils.parseGeoTranslation('schedules'),
15517							value: value
15518						});
15519					}
15520				}
15521			}
15522
15523//			// average time TODO with add to agenda??
15524//	    	if (model.geo_smart_rate && model.geo_smart_rate.average_time > 0) {
15525//	    		addGroup({
15526//	    			type: 'text',
15527//	    			name: 'average_time',
15528//	    			label: $translate.instant("msg.averageTime"),
15529//	    			value: model.geo_smart_rate.average_time + "m",
15530//					from: 'geo_smart_rate'
15531//				});
15532//	    	}
15533
15534			// columns
15535			if ($scope.meta && $scope.meta.columns) {
15536				var columns = $scope.meta.columns;
15537				for (var key in columns) {
15538					var column = columns[key];
15539					addColumn(model, column);
15540				}
15541			}
15542
15543			// properties
15544			var props = model.geo_properties;
15545			if (props) {
15546				for (var key in props) {
15547					addProperty(model, props[key]);
15548				}
15549			}
15550
15551			// images
15552			if (!hasMedia && !hasYoutube) {
15553				var imageList = $utils.getSorted(model.geo_media.images, '_order_num');
15554				if (imageList.length > 0) {
15555					addItem({
15556						force: true,
15557						big: true,
15558						type: 'images',
15559						medias: imageList
15560					}, true);
15561				}
15562			}
15563
15564			if (hasYoutube && (typeof YT == 'undefined')) {
15565				require(["https://www.youtube.com/iframe_api"], function(o) {
15566					parseDataEnd();
15567				});
15568				return;
15569			}
15570			parseDataEnd();
15571		}
15572
15573		function parseDataEnd() {
15574			if (data1.length === 0) {
15575				data1 = dataRight;
15576				dataRight = [];
15577			}
15578			$scope.data = data1;
15579			$scope.dataRight = dataRight;
15580			$angular.apply($scope);
15581		}
15582
15583		function addProperty(model, prop) {
15584			if ($utils.isTrue(prop.hidden)) {
15585				return;
15586			}
15587			if (prop.type == "boolean") {
15588				return;
15589			}
15590			addItem({
15591				type: "text",
15592				typeColumn: prop.type,
15593				name: prop.name,
15594				label: prop.name,//$utils.parseGeoTranslation(name, model)
15595				value: $utils.parse(prop.value)
15596			});
15597		}
15598
15599		function addColumn(model, column, innerEntity) {
15600			if (model == null) {
15601				return;
15602			}
15603			if (column.hidden || column.user) {
15604				return;
15605			}
15606			var def = $utils.isTrue(column.default);
15607			if (def) {
15608				return;
15609			}
15610
15611			var name = column.name;
15612			var type = column.type;
15613			var param = column.param;
15614			var html = $utils.isTrue(column.html);
15615			var nolabel = $utils.isTrue(column.nolabel);
15616			var value = model[name];
15617
15618			var pathToTranslations = 'geo_translations';
15619			if (innerEntity) {
15620				pathToTranslations += '.inner_entities.' + innerEntity;
15621			}
15622
15623			var item = {
15624				type: (html ? 'html' : 'text'),
15625				name: name,
15626				label: nolabel ? null : $utils.parseGeoTranslation(name, null, null, pathToTranslations),
15627				typeColumn: type
15628			};
15629			//console.log(type + ' - ' + name)
15630
15631			switch (type) {
15632
15633				case "any":
15634				case "cron":
15635				case "json":
15636				case "css":
15637					return;
15638
15639				case "boolean":
15640				case "boolean_int":
15641					item.type = 'text';
15642					if ($utils.isTrue(value)) {
15643						item.value = "\u2714";
15644					//} else {
15645					//	item.value = "\u2717";
15646					}
15647					break;
15648				
15649				case "button":
15650					item.click = function(e) {
15651						$angular.log("web item button not implemented");
15652					}
15653				case "link":
15654					item.type = type;
15655					item.value = $rootScope.columnButtonHref(column, $scope.model);
15656					item.blank = $rootScope.isColumnLinkBlank(column, $scope.model);
15657					item.icon = column.icon ? 'www/media/' + column.icon + '.img' : null;
15658					break;
15659
15660				case "entity":
15661				case "list":
15662					item.value = value;
15663					break;
15664
15665				case "inner_entity":
15666				case "inner_entity_list":
15667					if (!$utils.isBlank(column.inner_entity) && $scope.meta && $scope.meta.inner_entities) {
15668						var columns = $entity.getColumnsInner($scope.entity, column);
15669						if (columns && columns.length > 0) {
15670
15671							if (!$utils.isArray(value)) {
15672								value = [value];
15673							}
15674							for (var k in value) {
15675								var it = value[k];
15676								for (var key in columns) {
15677									addColumn(it, columns[key], column.inner_entity);
15678								}
15679							}
15680						}
15681					}
15682					return;
15683
15684				case "tags":
15685				case "string_list":
15686					if (value && $utils.isArray(value)) {
15687						item.value = value.join(", ");
15688					} else {
15689						item.value = value;
15690					}
15691					break;
15692					
15693				case "formula":
15694					if (column.formula && column.formula.startsWith('relation')) {
15695						item.type = "list";
15696						item.value = value;
15697					} else {
15698						item.value = $entity.displayColumn(model, column);
15699					}
15700					break;
15701
15702				case "select":
15703				case "enum":
15704					item.value = $utils.parseGeoTranslation(value, null, null, pathToTranslations + '.' + column.name + '$options');
15705					break;
15706
15707				case "enum_list":
15708					item.type = 'text_list';
15709					if (Array.isArray(value)) {
15710						item.value = value.map(function(val) {
15711							return $utils.parseGeoTranslation(val, null, null, pathToTranslations + '.' + column.name + '$options');
15712						});
15713					}
15714					break;
15715
15716				case "image":
15717					var images = [];
15718					if (value && model.geo_media && model.geo_media.images) {
15719						(Array.isArray(value) ? value : [value]).forEach(function(mediaId) {
15720							var image = mediaId ? model.geo_media.images[mediaId] : null;
15721							if (image) {
15722								images.push(image);
15723							}
15724						});
15725					}
15726					if (images.length !== 0) {
15727						hasMedia = true;
15728						addItem({
15729							label: item.label,
15730							force: true,
15731							type: 'images',
15732							medias: images
15733						});
15734					}
15735					return;
15736
15737				case "audio":
15738					var audios = [];
15739					if (value && model.geo_media && model.geo_media.audio) {
15740						(Array.isArray(value) ? value : [value]).forEach(function(mediaId) {
15741							var audio = mediaId ? model.geo_media.audio[mediaId] : null;
15742							if (audio) {
15743								audios.push({
15744									name: audio.name ? audio.name : mediaId,
15745									url: 'www/media/' + mediaId + '.audio'
15746								});
15747							}
15748						});
15749					}
15750					if (audios.length !== 0) {
15751						hasMedia = true;
15752						addItem({
15753							label: item.label,
15754							typeColumn: 'audio',
15755							big: audios.length > 1,
15756							type: audios.length === 1 ? 'audio' : 'audio-playlist',
15757							value: audios[0].url,
15758							medias: audios
15759						});
15760					}
15761					return;
15762
15763				case "video":
15764					var ownVideos = [];
15765					var youtubeVideos = [];
15766					if (value && model.geo_media && model.geo_media.video) {
15767						var videos = [];
15768						if (Array.isArray(value)) {
15769							videos = value;
15770						} else {
15771							videos.push(value);
15772						}
15773						videos.forEach(function(mediaId) {
15774							var video = mediaId ? model.geo_media.video[mediaId] : null;
15775							if (video) {
15776								if (video.extension !== 'youtube') {
15777									ownVideos.push({
15778										name: video.name ? video.name : mediaId,
15779										url: 'www/media/' + mediaId + '.video'
15780									});
15781								} else if (video.source_media_url) {
15782									youtubeVideos.push({
15783										name: video.name ? video.name : video.source_media_url,
15784										url: video.source_media_url
15785									});
15786								}
15787							}
15788						});
15789					}
15790					if (ownVideos.length !== 0) {
15791						hasMedia = true;
15792						addItem({
15793							label: item.label,
15794							typeColumn: 'video',
15795							big: true,
15796							type: ownVideos.length === 1 ? 'video' : 'video-playlist',
15797							value: ownVideos[0].url,
15798							medias: ownVideos
15799						});
15800					}
15801					if (youtubeVideos.length !== 0) {
15802						hasYoutube = true;
15803						addItem({
15804							label: item.label,
15805							typeColumn: 'video',
15806							big: true,
15807							type: youtubeVideos.length === 1 ? 'youtube' : 'youtube-playlist',
15808							value: youtubeVideos[0].url,
15809							medias: youtubeVideos
15810						});
15811					}
15812					return;
15813
15814				case "document":
15815					if (value && model.geo_media && model.geo_media.others) {
15816						var docs = [];
15817						if (Array.isArray(value)) {
15818							docs = value;
15819						} else {
15820							docs.push(value);
15821						}
15822						var ivalue = "";
15823						docs.forEach(function(mediaId) {
15824							var doc = model.geo_media.others[mediaId];
15825							if (doc) {
15826								ivalue += '<a target="_blank" href="' + contextPath + '/www/media/' + mediaId + '.' + doc.extension +'">' +
15827									doc.name + '.' + doc.extension + '</a>';
15828							}
15829						});
15830						if (ivalue != "") {
15831							item.type = 'html';
15832							item.value = ivalue;
15833						}	
15834					}
15835					break;
15836
15837				case 'chart':
15838					if (column.datamixer) {
15839						addItem({
15840							big: true,
15841							force: true,
15842							type: 'chart',
15843							datamixer: column.datamixer
15844						});
15845					}
15846					return;
15847
15848				default:
15849					item.value = $entity.displayColumn(model, column, html);
15850					break;
15851			}
15852
15853			addItem(item);
15854		}
15855
15856		function addItem(item, addToFirst) {
15857
15858			if (item.typeColumn === 'section') {
15859				item.type = "section";
15860
15861			} else if (item.typeColumn === 'message') {
15862				item.value = item.label;
15863				item.label = null;
15864
15865			} else if (item.typeColumn !== 'video') {
15866
15867				if (item.value && (typeof item.value == 'string') && item.value.startsWith("http")) {
15868					item.type = "link";
15869
15870					if (item.value.contains("youtube.com")) {
15871						item.type = "youtube";
15872						item.big = true;
15873						addToFirst = true;
15874						hasYoutube = true;
15875					}
15876				}
15877
15878				if (!$utils.isTrue(item.force) && $utils.isBlank(item.value)) {
15879					return;
15880				}
15881
15882				if (item.typeColumn === 'entity') {
15883					if (typeof item.value == 'string') {
15884						return; // can be instances
15885					}
15886					item.type = 'list';
15887					item.value = [item.value];
15888				}
15889				if (item.typeColumn === 'list') {
15890					item.type = 'list';
15891				}
15892				if (item.type === 'list') {
15893					// validate list
15894					var l = item.value;
15895					var l2 = [];
15896					for (var k in l) {
15897						var o = l[k];
15898						if ($entity.isItemValid($scope.entity, o)) {
15899							l2.push(o);
15900							//console.log(o);
15901						}
15902					}
15903					if (l2.length === 0) {
15904						return;
15905					}
15906					if (l2.length > 1) {
15907						item.big = true;
15908					}
15909					item.value = l2;
15910
15911					var mapPoints = l2.map(function(listItem) {
15912						var geoLocation = listItem.geo_location;
15913						if (geoLocation != null && !!geoLocation.latitude && !!geoLocation.longitude) {
15914							return {
15915								title: $entity.displayColumn(listItem),
15916								latitude: geoLocation.latitude,
15917								longitude: geoLocation.longitude
15918							};
15919						}
15920						return null;
15921					}).filter(function(mapPoint) {
15922						return !!mapPoint;
15923					});
15924					if (mapPoints.length > 0) {
15925						addItem({
15926							force: true,
15927							label: item.label,
15928							type: 'map',
15929							points: mapPoints
15930						});
15931					}
15932				}
15933			}
15934
15935			if (item.type === "html" || item.type === "text") {
15936				if (item.value && item.value.length > 800) {
15937					item.big = true;
15938				}
15939			} else if (item.type === "boolean" || item.type === "boolean_int") {
15940				if (item.value) {
15941					item.value = "*";
15942				} else {
15943					item.value = "";
15944				}
15945			}
15946
15947			var isBig = $utils.isTrue(item.big);
15948			if (isBig || !$utils.isScreenLarge) {
15949				if (addToFirst) {
15950					data1.unshift(item);
15951				} else {
15952					data1.push(item);
15953				}
15954			} else {
15955				if (addToFirst) {
15956					dataRight.unshift(item);
15957				} else {
15958					dataRight.push(item);
15959				}
15960			}
15961		}
15962
15963		function getTimeTables(list) {
15964			var result = "<table>";
15965			if (list) {
15966				var $translate = $angular.get("$translate");
15967				for (var i in list) {
15968					var item = list[i];
15969					if (item && item.day) {
15970						result += "<tr><td>" + $translate.instant($constants.daysList[item.day])
15971							+ ": </td><td>" + item.time_from + " - " + item.time_to + "</td></tr>";
15972					}
15973				}
15974			}
15975			result += "</table>";
15976			return result;
15977		}
15978
15979		// share
15980		$scope.share = function() {
15981			var link = "" + window.location.href;
15982			navigator.share({
15983				title: 'Share',
15984				url: link
15985			}).then(function() {
15986				//console.log('Thanks for sharing!');
15987			});
15988		};
15989	}
15990})();
15991
15992//// File geolocation.service.js ////
15993(function() {
15994	'use strict';
15995
15996	$angular.factories['geolocationService'] = function($window, $q) {
15997		var currentPosition;
15998		var watching = false;
15999		var service = {};
16000
16001		service.watchCurrentLocation = function() {
16002			if (!watching) {
16003				if (!$window.navigator.geolocation) {
16004					console.warn('Geolocation not supported.');
16005				} else {
16006					$window.navigator.geolocation.watchPosition(function(position) {
16007						watching = true;
16008						currentPosition = position ? position.coords : null;
16009						var geoLocationCookie = currentPosition.latitude + ',' + currentPosition.longitude;
16010						$cookiesService.put('geo_location', geoLocationCookie, 1);
16011					});
16012				}
16013			}
16014		};
16015
16016		service.getCurrentLocation = function() {
16017			var deferred = $q.defer();
16018
16019			if (watching && currentPosition) {
16020				deferred.resolve(currentPosition);
16021			} else {
16022				if (!$window.navigator.geolocation) {
16023					deferred.reject('Geolocation not supported.');
16024				} else {
16025					$window.navigator.geolocation.getCurrentPosition(function(position) {
16026						currentPosition = position ? position.coords : null;
16027						deferred.resolve(currentPosition);
16028					}, function(err) {
16029						deferred.reject(err);
16030					});
16031				}
16032			}
16033			return deferred.promise;
16034		};
16035
16036		return service;
16037	};
16038})();
16039
16040//// File preferences.js ////
16041(function() {
16042    
16043    
16044    $angular.controllers['preferences'] = function($scope, $translate) {
16045    	$angular.preparePage();
16046    	$angular.contentScope = $scope;
16047    	$angular.menuScope.selectedPage = 'preferences';
16048    	
16049//    	$scope.setTitle = function() {
16050//        	$angular.setTitle($translate.instant("msg.preferences"));
16051//    	};
16052//    	$scope.setTitle();
16053    	
16054    	$scope.licenses = [];
16055    	
16056    	var path = "/www/license.json";
16057    	$rest.get({
16058			skipHideLoading: true,
16059			url: contextPath + path + "?instances="+$instance.getId(),
16060			loading: "generic"
16061		}).then(function(o) {
16062	    	$scope.licenses = o.list;
16063		});
16064    	
16065	}
16066	
16067})();
16068
16069//// File logout.js ////
16070(function() {
16071    
16072    $angular.controllers['logout'] = function($rootScope, $scope) {
16073    	$angular.preparePage();
16074    	$angular.contentScope = $scope;
16075    	
16076    	$rest.get({
16077    		loading: "generic",
16078    		url: contextPath + "/log-out?_r=" + new Date().getTime()
16079    	}).then(function() {
16080    		$user.current = null;
16081    		if (!$constants.isWeb) {
16082    			$instance.current = null;
16083    		}
16084    		
16085			if ($instance.isMslogin()) {
16086				$cookiesService.remove("ms_access_token");
16087				$cookiesService.put("instance", $instance.getId(), 1);
16088				window.location.href = "ms/a/logout.go";
16089				return;
16090			}
16091    		
16092    		$angular.goHome();
16093			$cart.initCart();
16094    	});
16095	}
16096})();
16097
16098//// File debounce.js ////
16099(function() {
16100	'use strict';
16101
16102	$angular.factories['debounce'] = function($timeout, $q) {
16103		// The service is actually this function, which we call with the func
16104		// that should be debounced and how long to wait in between calls
16105		return function debounce(func, wait, immediate) {
16106			var timeout;
16107			// Create a deferred object that will be resolved when we need to
16108			// actually call the func
16109			var deferred = $q.defer();
16110			return function() {
16111				var context = this, args = arguments;
16112				var later = function() {
16113					timeout = null;
16114					if (!immediate) {
16115						deferred.resolve(func.apply(context, args));
16116						deferred = $q.defer();
16117					}
16118				};
16119				var callNow = immediate && !timeout;
16120				if (timeout) {
16121					$timeout.cancel(timeout);
16122				}
16123				timeout = $timeout(later, wait);
16124				if (callNow) {
16125					deferred.resolve(func.apply(context, args));
16126					deferred = $q.defer();
16127				}
16128				return deferred.promise;
16129			};
16130		};
16131	};
16132})();
16133
16134//// File not-found.js ////
16135(function() {
16136    
16137    
16138    $angular.controllers['not-found'] = function($scope, $location, $translate) {
16139    	//$angular.preparePage();
16140    	$angular.contentScope = $scope;
16141    	$angular.goHome();
16142    	//$utils.showNotFound();
16143	}
16144	
16145})();
16146
16147//// File login.js ////
16148(function() {
16149	
16150	$angular.controllers['login'] = function($scope) {
16151		$angular.preparePage();
16152		$angular.contentScope = $scope;
16153	}
16154})();
16155
16156//// File panel-buy.js ////
16157(function() {
16158	
16159	$angular.controllers['panel-buy'] = function($uibModalInstance, $scope, params) {
16160		$scope.item = params.item;
16161		$scope.cartItem = {
16162			quantity: params.cartItem ? params.cartItem.quantity : 1,
16163			extraData: params.cartItem ? params.cartItem.extra_data : {}
16164		};
16165		$scope.ready = false;
16166
16167		$scope.stock = $cart.getStock($scope.item);
16168
16169		var formEntity = $scope.item.geo_buy.form_entity;
16170		if (formEntity && $entity.isGeoForm(formEntity) && $entity.isPermitted(formEntity, 'create')) {
16171			$scope.extraDataEntity = $entity.getEntity(formEntity);
16172		} else {
16173			$scope.ready = true;
16174		}
16175
16176		$scope.onChangeExtraData = function(extraData, isReady) {
16177			$scope.ready = isReady;
16178			$scope.cartItem.extraData = extraData;
16179		};
16180
16181		$scope.close = function() {
16182			$uibModalInstance.dismiss('cancel');
16183		};
16184
16185		$scope.addQuantity = function(item) {
16186			if ($cart.hasStock(item, $scope.cartItem.quantity)) {
16187				$scope.cartItem.quantity++;
16188			}
16189		};
16190
16191		$scope.removeQuantity = function() {
16192			if ($scope.cartItem.quantity === 1) {
16193				return;
16194			}
16195			$scope.cartItem.quantity--;
16196		};
16197
16198		$scope.buy = function() {
16199			$cart.addItem($scope.item.entity, $scope.item.id, $scope.cartItem);
16200			if (!$angular.element("#cartAsMenu").hasClass("menu-open")) {
16201				$events.toggleCartAsMenu();
16202			}
16203			$scope.close();
16204		};
16205	}
16206})();
16207
16208//// File panel-add-instance.js ////
16209(function() {
16210    
16211    
16212    $angular.controllers['panel-add-instance'] = function($uibModalInstance, $scope, $translate, $http, params) {
16213		
16214		$scope.model = {};
16215		
16216		$scope.close = function() {
16217			$uibModalInstance.dismiss('cancel');
16218		}
16219
16220		$scope.create = function(form) {
16221			if (!$utils.validateForm(form)) {
16222				return;
16223			}
16224			
16225			var id = $instance.current.add_subinstance;
16226			
16227			var data = {
16228				name: $scope.model.name,
16229				copyContent: true,
16230				copySystem:  true
16231			};
16232
16233			$rest.post({
16234    			loading: "generic",
16235    			url: contextPath + "/api/geo-new/instance/" + id + "/copy.json",
16236				data: data,
16237    			successMessage: $angular.get("$translate").instant('msg.created')
16238    		}).then(function(o) {
16239				$scope.close();
16240				$instance.refresh();
16241    		});
16242
16243		}
16244		
16245	}
16246	
16247})();
16248
16249//// File panel-edit-media.js ////
16250(function () {
16251    
16252    $angular.controllers['panel-edit-media'] = function ($uibModalInstance, $scope, $translate, $http, params, $timeout) {
16253        var type = params.type;
16254
16255        $timeout(function () {
16256            $scope.model = params.data;
16257            $scope.model.mediaType = type;
16258            $scope.mediaType = type;
16259            $scope.downloadExtension = type;
16260            if (type === 'images') {
16261                $scope.downloadExtension = 'io';
16262            } else if (type === 'others') {
16263                $scope.downloadExtension = $scope.model.extension;
16264            }
16265            $scope.mediaI18n = {};
16266            $instance.languages.forEach(function(lang) {
16267                $scope.mediaI18n[lang] = 'loading';
16268            });
16269            checkMediaI18n();
16270	        loadAppMedia();
16271        });
16272        
16273
16274        $scope.close = function () {
16275            $uibModalInstance.dismiss();
16276        }
16277
16278        $scope.saveMediaInfo = function () {
16279            $rest.put({
16280                url: contextPath + '/api/media/' + $scope.model.id + '.json',
16281                data: $scope.model,
16282                successMessage: $angular.get("$translate").instant('msg.updated')
16283            }).then(function () {
16284                $uibModalInstance.close();
16285            });
16286        };
16287
16288        $scope.uploadTranslation = function(lang) {
16289            $angular.openModal("panel-add-media", {
16290                entity: $angular.contentScope.entity,
16291                id: $angular.contentScope.entityId,
16292                type: type,
16293                size: 'xl',
16294                mediaId: $scope.model.id,
16295                language: lang
16296            }).then(function() {
16297                checkMediaI18n();
16298            });
16299        };
16300
16301        $scope.removeTranslation = function(lang) {
16302            $rest.remove({
16303                url: contextPath + '/api/media/' + $scope.model.id + '.json?language=' + lang,
16304                loading: "generic",
16305                confirmTitle: $angular.get("$translate").instant("msg.remove"),
16306                successMessage: $angular.get("$translate").instant('msg.removed')
16307            }).then(function() {
16308                checkMediaI18n();
16309            });
16310        };
16311
16312        function checkMediaI18n() {
16313            $scope.rand = Math.random();
16314            $instance.languages.forEach(function(lang) {
16315                $rest.get({
16316                    url: contextPath + '/api/media/' + $scope.model.id + '.json?language=' + lang,
16317                    showError: false
16318                }).then(function () {
16319                    $scope.mediaI18n[lang] = 'found';
16320                }).catch(function () {
16321                    $scope.mediaI18n[lang] = 'not-found';
16322                });
16323            });
16324        }
16325        
16326        function loadAppMedia() {
16327            $rest.get({
16328                url: contextPath + '/api/media/' + $scope.model.id + '/appMedia.json',
16329                showError: false
16330            }).then(function (o) {
16331                $scope.info = o;
16332            });
16333        }
16334    }
16335
16336})();
16337
16338//// File panel-upload.js ////
16339(function() {
16340
16341	$angular.controllers['panel-upload'] = function($uibModalInstance, $scope, $translate, params) {
16342
16343		$scope.close = function() {
16344			$uibModalInstance.dismiss('cancel');
16345		};
16346
16347		if (params.type === 'batch') {
16348
16349			var dz = {
16350				options : {
16351					url: contextPath + "/api/batch.json?task="+ $angular.contentScope.taskUpload + "&arg=" + $angular.contentScope.arg3,
16352					paramName : 'file', acceptedFiles : '', autoProcessQueue: true, maxFilesize : '10000', // MB
16353					addRemoveLinks: true, parallelUploads: 1, timeout: 3600000, // ms
16354					dictDefaultMessage : $translate.instant("msg.dropzone.drop")
16355				},
16356				methods : {},
16357				callbacks : {
16358					sending : function(file, xhr, formData) {},
16359	    		    addedfile : function(file) {},
16360	    		    removedfile : function(file) {},
16361	    		    success : function(file, data) {
16362	    		    	if (data && data._logs) {
16363							$angular.consoleScope.addLog(data._logs);
16364							delete data._logs;
16365						}
16366						$scope.dzMethods.getDropzone().removeFile(file);
16367	    		    	$utils.floatingMsg("Uploaded " + file.name);
16368						if (params.successFunction) {
16369							params.successFunction(file, data);
16370						}
16371	    		    },
16372	    		    error : function(file, response) {
16373	    		        $utils.floatingError((response && response.message) ? response.message : $translate.instant("msg.error"));
16374	    		    },
16375	    		    queuecomplete : function() {
16376						$uibModalInstance.dismiss('cancel');
16377	    		    }
16378				}
16379			};
16380
16381			$scope.getDzOptions = function() {
16382				return dz.options;
16383			}
16384			$scope.dzCallbacks = dz.callbacks;
16385			$scope.dzMethods = dz.methods;
16386
16387		} else if (params.spreadsheet) {
16388			
16389			$angular.rootScope.dzOptions.url = 'api/geo/' + $angular.contentScope.entity
16390				+ '/a/uploadList.json?spreadsheet=' + params.spreadsheet.id 
16391				+ '&geo_instance=' + $instance.getId() 
16392				+ '&preview=' + !params.spreadsheet.no_preview;
16393			$angular.contentScope.dropzoneSuccessFile = function(file, data) {
16394				if (data && data._logs) {
16395					$angular.consoleScope.addLog(data._logs);
16396					delete data._logs;
16397				}
16398				$uibModalInstance.dismiss('cancel');
16399				if (data.preview) { 
16400					showPreview(file, data);
16401				} else {
16402					$angular.contentScope.refresh();
16403				}
16404				$instance.hasToRefresh = true;
16405			};
16406		
16407		} else {
16408			
16409			// generic upload
16410			$angular.rootScope.dzOptions.url = 'api/geo/' + $angular.contentScope.entity
16411				+ '/a/uploadList.json?geo_instance=' + $instance.getId();
16412			$angular.contentScope.dropzoneSuccessFile = function(file, data) {
16413				if (data && data._logs) {
16414					$angular.consoleScope.addLog(data._logs);
16415					delete data._logs;
16416				}
16417				$uibModalInstance.dismiss('cancel');
16418				$angular.contentScope.refresh();
16419				$instance.hasToRefresh = true;
16420			};
16421			
16422		}
16423
16424		function showPreview(file, response) {
16425			var meta = $entity.getEntity(response.entity);
16426			$angular.openModalLocal('panel-columns', {
16427				title: 'msg.upload.preview',
16428				meta: meta,
16429				columns: meta.columns,
16430				model: response.preview,
16431				opts: {readonly: true},
16432				buttons: [
16433					{result: 'cancel', class: 'btn-default', text: 'msg.cancel'},
16434					{result: 'execute', class: 'btn-primary', text: 'msg.upload.execute'}
16435				],
16436				size: 'xl'
16437			}).then(function(result) {
16438				if (result === 'execute') {
16439					saveSpreadsheetFile(file, response.spreadsheet);
16440				} else if (result === 'cancel') {
16441					$angular.contentScope.refresh();
16442				}
16443			}, function() {
16444				$angular.contentScope.refresh();
16445			})
16446		}
16447
16448		function saveSpreadsheetFile(file, spreadsheet) {
16449			var reader = new FileReader();
16450			reader.onload = function(event) {
16451				var fileData = {
16452					base64: event.target.result,
16453					name: file.name,
16454					type: file.type
16455				};
16456				$rest.post({
16457					loading: 'generic',
16458					url: contextPath + '/api/geo/' + $angular.contentScope.entity + '/a/uploadList.json?spreadsheet=' 
16459						+ spreadsheet + '&geo_instance=' + $instance.getId(),
16460					data: {
16461						fileBase64: fileData
16462					}
16463				}).then(function() {
16464					$angular.contentScope.refresh();
16465				}, function() {
16466					$angular.contentScope.refresh();	
16467				});
16468			};
16469			reader.readAsDataURL(file);
16470		}
16471	}
16472})();
16473
16474//// File panel-add-media.js ////
16475(function() {
16476    
16477    
16478    $angular.controllers['panel-add-media'] = function($uibModalInstance, $scope, $translate, $http, params, $timeout) {
16479    	$scope.close = function() {
16480			$uibModalInstance.dismiss('cancel');
16481		}
16482    	$scope.entity = params.entity;
16483    	$scope.entityId = params.id;
16484    	$scope.mediaType = params.type;
16485    	$scope.images = [];
16486    	
16487    	function closeAndRefresh() {
16488    		$uibModalInstance.close();
16489    		if (typeof $angular.contentScope.refreshMedia != 'undefined') {
16490    			$angular.contentScope.refreshMedia();
16491    			return;
16492    		}
16493    		if (typeof $angular.contentScope.refreshForm != 'undefined') {
16494    			$angular.contentScope.refreshForm();
16495    			return;
16496    		}
16497    		if (typeof $angular.contentScope.refresh != 'undefined') {
16498    			$angular.contentScope.refresh();
16499    			return;
16500    		}
16501    	}
16502    	
16503    	// dropzone
16504    	$angular.rootScope.dzOptions.url = (!$constants.isWeb ? 'api' : 'www') + '/media.json';
16505		if ($instance.current) {
16506			$angular.rootScope.dzOptions.headers = { 'From' : $instance.getId() };
16507		} else {
16508			$angular.rootScope.dzOptions.headers = null;
16509		}
16510    	$angular.contentScope.dropzoneSending = function(file, xhr, formData) {
16511			if (params.entity) formData.append('entity', params.entity);
16512			if (params.id) formData.append('entityId', params.id);
16513			if (params.type) formData.append('type', params.type);
16514			if (params.column) formData.append('column', params.column);
16515			if (params.mediaId) formData.append('mediaId', params.mediaId);
16516			if (params.language) formData.append('language', params.language);
16517    	};
16518    	$angular.contentScope.dropzoneCallback = function() {
16519    		closeAndRefresh();
16520    	};
16521    	
16522    	setTimeout(function() {
16523    		//$("#uploadMedias div.dropzone").click();
16524    	}, 100);
16525    	if ($constants.isWeb) {
16526    		return;
16527    	}
16528    	
16529    	// url
16530    	$scope.saveImageUrl = function() {
16531    		if ($utils.isBlank($("#imageUrlForm input.url").val())) {
16532        		return;
16533        	}
16534        	
16535        	$rest.post({
16536        		form: $("#imageUrlForm"),
16537        		url: contextPath + '/api/media/a/createUrl.json',
16538        		loading: "generic",
16539        		successMessage: $angular.get("$translate").instant('msg.saved')
16540        	}).then(function(data, url) {
16541        		closeAndRefresh();
16542        	});
16543    	};
16544    	
16545    	// edit images
16546    	$scope.removeImage = function(e, item) {
16547    		$utils.preventDefault(e);
16548    		item.removed = true;
16549    	};
16550    	$scope.showImage = function(e, item) {
16551    		$utils.preventDefault(e);
16552    		$angular.openModalLocal("panel-image", {data: item});
16553    	};
16554    	
16555    	// save images
16556    	$scope.saveImages = function() {
16557    		$rest.post({
16558        		form: $("#confirmImages"),
16559        		url: contextPath + '/' + (!$constants.isWeb ? 'api' : 'www') + '/media.json',
16560        		loading: "generic",
16561        		successMessage: $angular.get("$translate").instant('msg.saved')
16562        	}).then(function(data, url) {
16563        		closeAndRefresh();
16564        	});
16565    	};
16566    	
16567    	// wikipedia
16568    	$scope.wikiPage = null;
16569    	$scope.searchWikiPages = function(value) {
16570    		//console.log(value)
16571    		if ($utils.isBlank(value) || value.length < 3) {
16572    			return;
16573    		}
16574    		$rest.get({
16575    			url : contextPath + '/api/geo-new/'+$angular.contentScope.entity+'/a/searchWikiPages.json?name='+value,
16576    			loading : 'generic'
16577        	}).then(function(data) {
16578        		$scope.wikiPage = null;
16579        		$angular.rootScope.wikiPages = data.wikiPages;
16580        	});
16581    	};
16582		$scope.selectWikiPage = function(value) {
16583			$("#imagesWikipediaForm input.name").val(value);
16584			$rest.post({
16585        		form: $("#imagesWikipediaForm"),
16586        		url: contextPath + '/api/media/a/imagesWikipedia.json',
16587        		loading: "generic"
16588        	}).then(function(data, url) {
16589        		$scope.morePages = false;
16590        		$scope.images = data.list;
16591        	});
16592		};
16593    	
16594    	// google
16595    	$timeout(function() {
16596        	if ($("#google-images").length > 0) {
16597        		var autocomplete = new google.maps.places.Autocomplete(document.getElementById('google-images'), $instance.getSearchOptions());
16598            	google.maps.event.addListener(autocomplete, 'place_changed', function() {
16599            		var place = autocomplete.getPlace();
16600                	if (place == null || place.geometry == null) {
16601                		return;
16602                	}
16603                	var id = place.place_id;
16604                	$("#imagesGoogleForm input.name").val(id);
16605                	$rest.post({
16606                		form: $("#imagesGoogleForm"),
16607                		url: contextPath + '/api/media/a/imagesGoogle.json',
16608                		loading: "generic"
16609                	}).then(function(data, url) {
16610                		$scope.morePages = false;
16611                		$scope.images = data.list;
16612                	});
16613              	});
16614        	}
16615    	}, 500);
16616    	
16617    	// flickr
16618    	$scope.getImagesFlickr = function(add) {
16619    		var page = "";
16620    		if (add) {
16621    			page = ($scope.images.length / 20) + 1;
16622    		}
16623    		$rest.post({
16624        		form: $("#imagesFlickrForm"),
16625        		url: contextPath + '/api/media/a/imagesFlickr.json?page='+page,
16626        		loading: "generic"
16627        	}).then(function(data, url) {
16628        		$scope.morePages = false;
16629        		if (add) {
16630        			var num = $scope.images.length;
16631        			$scope.images = $scope.images.concat(data.list);
16632        			if ($scope.images.length == (num+20)) {
16633            			$scope.morePages = true;
16634        			}
16635        		} else {
16636            		$scope.images = data.list;
16637            		if ($scope.images.length == 20) {
16638            			$scope.morePages = true;
16639            		}
16640        		}
16641        	});
16642    	};
16643
16644		$scope.moreFlickr = function() {
16645			$scope.getImagesFlickr(true);
16646		};
16647
16648		// youtube url
16649		$scope.saveYoutubeUrl = function(youtubeUrl) {
16650			if ($utils.isBlank(youtubeUrl)) {
16651				return;
16652			}
16653			var data = {
16654				entity: $scope.entity,
16655				entityId: $scope.entityId,
16656				url: youtubeUrl
16657			};
16658			if (params.mediaId) {
16659				data.mediaId = params.mediaId;
16660			}
16661			if (params.language) {
16662				data.language = params.language;
16663			}
16664			$rest.post({
16665				url: contextPath + '/api/media/a/createYoutubeUrl.json',
16666				data: data,
16667				loading: "generic",
16668				successMessage: $angular.get("$translate").instant('msg.saved')
16669			}).then(function() {
16670				closeAndRefresh();
16671			});
16672		};
16673		
16674		// TTS: https://cloud.ibm.com/apidocs/text-to-speech?code=node#synthesize
16675		$scope.tts = {
16676			entity: $scope.entity,
16677			entityId: $scope.entityId,
16678			language: $locale.language,
16679			voice: "",
16680			text: ""
16681		};
16682		$scope.saveTTS = function() {
16683			$rest.post({
16684				url: contextPath + '/api/media/a/createTTS.json',
16685				data: $scope.tts,
16686				loading: "generic",
16687				successMessage: $angular.get("$translate").instant('msg.saved')
16688			}).then(function() {
16689				closeAndRefresh();
16690			});
16691		};
16692		$scope.voices = {
16693			'en': [ 'en-AU-CraigVoice', 'en-AU-MadisonVoice', 'en-GB_CharlotteV3Voice', 'en-GB_JamesV3Voice', 'en-GB_KateVoice', 'en-GB_KateV3Voice', 'en-US_AllisonVoice', 'en-US_AllisonV3Voice', 'en-US_EmilyV3Voice', 'en-US_HenryV3Voice', 'en-US_KevinV3Voice', 'en-US_LisaVoice', 'en-US_LisaV3Voice', 'en-US_MichaelVoice', 'en-US_MichaelV3Voice', 'en-US_OliviaV3Voice' ],	
16694			'es': [ 'es-ES_EnriqueVoice', 'es-ES_EnriqueV3Voice', 'es-ES_LauraVoice', 'es-ES_LauraV3Voice', 'es-LA_SofiaVoice', 'es-LA_SofiaV3Voice', 'es-US_SofiaVoice', 'es-US_SofiaV3Voice' ],	
16695			'fr': [ 'fr-FR_NicolasV3Voice', 'fr-FR_ReneeVoice', 'fr-FR_ReneeV3Voice' ],
16696			'it': [ 'it-IT_FrancescaVoice', 'it-IT_FrancescaV3Voice' ],
16697			'de': [ 'de-DE_BirgitVoice', 'de-DE_BirgitV3Voice', 'de-DE_DieterVoice', 'de-DE_DieterV3Voice', 'de-DE_ErikaV3Voice' ],	
16698		};
16699		
16700		
16701	}
16702	
16703	$angular.controllers['panel-image'] = function($uibModalInstance, $scope, $translate, $http, params, $timeout) {
16704    	$timeout(function() {
16705        	$scope.item = params.data;
16706    	});
16707		$scope.close = function() {
16708			$uibModalInstance.dismiss('cancel');
16709		}
16710	}
16711	
16712})();
16713
16714//// File panel-generic.js ////
16715(function() {
16716    
16717    
16718    $angular.controllers['panel-generic'] = function($uibModalInstance, $scope, $translate, $http, params, $sce) {
16719		
16720		$scope.title = params.title;
16721		$scope.model = params.model; 
16722		$scope.content = $sce.trustAsHtml(params.content);
16723		//console.log(params.content)
16724		
16725		$scope.close = function() {
16726			$uibModalInstance.dismiss('cancel');
16727		}
16728
16729		$scope.accept = function() {
16730			var result = params.successFunction($scope.model);
16731			if (result) {
16732				result.then(function() {
16733					$uibModalInstance.dismiss('success');
16734				});
16735			} else {
16736				$uibModalInstance.dismiss('success');
16737			}
16738    	};
16739		
16740	}
16741	
16742})();
16743
16744//// File panel-message.js ////
16745(function() {
16746
16747	$angular.controllers['panel-message'] = function($uibModalInstance, $scope, $translate, params) {
16748		$scope.title = $translate.instant(params.title);
16749		$scope.text = $translate.instant(params.text);
16750
16751		$scope.close = function() {
16752			$uibModalInstance.close();
16753		}
16754	};
16755})();
16756
16757//// File panel-select-media-image.js ////
16758(function() {
16759    
16760    
16761    $angular.controllers['panel-select-media-image'] = function($uibModalInstance, $scope, params) {
16762
16763    	$scope.images = params.model && params.model.geo_media && params.model.geo_media.images ? params.model.geo_media.images : null;
16764		$scope.callback = params.callback;
16765
16766		$scope.close = function() {
16767			$uibModalInstance.dismiss('cancel');
16768		}
16769
16770		$scope.selectImage = function(image) {
16771			$scope.close();
16772			$scope.callback(image);
16773		}
16774	}
16775})();
16776
16777//// File panel-query-builder.js ////
16778(function() {
16779	$angular.controllers['panel-query-builder'] = function($uibModalInstance, $scope, params, debounce) {
16780		$scope.filterOperators = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'like', 'exists', 'in', 'nin'];
16781		$scope.orderDirections = [{value: 'asc', label: 'msg.direction.asc'}
16781, {value: 'desc', label: 'msg.direction.desc'}];
16782		$scope.entity = params.entity;
16783		$scope.innerEntity = params.innerEntity;
16784		$scope.fromEntity = params.fromEntity;
16785		$scope.columns = {};
16786		$scope.model = parseQuery($scope, params.query);
16787
16788		$scope.$watch('model', debounce(function(data) {
16789			applyFiltersChanges($scope, data.filters);
16790			$scope.query = formatQuery(data);
16791		}, 500), true);
16792
16793		$scope.close = function() {
16794			$uibModalInstance.dismiss();
16795		};
16796
16797		$scope.finish = function() {
16798			applyFiltersChanges($scope, $scope.model.filters);
16799			$scope.query = formatQuery($scope.model);
16800			$uibModalInstance.close($scope.query);
16801		};
16802
16803		$scope.clear = function() {
16804			$scope.model = parseQuery($scope);
16805			prepareDefaults($scope);
16806		};
16807
16808		$scope.addNewFilter = function() {
16809			var operator = 'eq';
16810			var multiValue = toMultiValue(null);
16811			var filter = {
16812				operator: operator,
16813				_operator: operator,
16814				multiValue: multiValue,
16815				_multiValue: $utils.clone(multiValue)
16816			};
16817			addFilterTemplate($scope, filter);
16818			$scope.model.filters.push(filter);
16819		};
16820		$scope.addNewOrder = function() {
16821			$scope.model.orders.push({
16822				direction: 'asc'
16823			});
16824		};
16825		$scope.addNewProjection = function() {
16826			$scope.model.projections.push({});
16827		};
16828
16829		prepareDefaults($scope);
16830		$scope.query = formatQuery($scope.model);
16831	};
16832
16833	function prepareDefaults($scope) {
16834		if ($scope.model.filters.length === 0) {
16835			$scope.addNewFilter();
16836		}
16837		if ($scope.model.orders.length === 0) {
16838			$scope.addNewOrder();
16839		}
16840		if ($scope.model.projections.length === 0) {
16841			$scope.addNewProjection();
16842		}
16843	}
16844
16845	function parseQuery($scope, query) {
16846		var filters = [];
16847		var limitEnabled = false;
16848		var limit = null;
16849		var orderEnabled = false;
16850		var orders = [];
16851		var projectionEnabled = false;
16852		var projections = [];
16853		if (query) {
16854			query.split("&").forEach(function(param) {
16855				var paramParts = param.split('=');
16856				if (paramParts.length === 2) {
16857					var paramKey = paramParts[0];
16858					var paramValue = paramParts[1];
16859					if (paramKey && paramValue) {
16860						if (paramKey === '_limit') {
16861							if ($utils.isNumeric(paramValue)) {
16862								limitEnabled = true;
16863								limit = Number(paramValue);
16864							}
16865						} else if (paramKey === '_order') {
16866							orderEnabled = true;
16867							orders = paramValue.split(',').map(function(orderParam) {
16868								var orderParts = orderParam.split(":");
16869								return {
16870									path: orderParts[0],
16871									direction: (orderParts.length > 1 && orderParts[1] === 'desc') ? 'desc' : 'asc'
16872								};
16873							});
16874						} else if (paramKey === '_projection') {
16875							projectionEnabled = true;
16876							projections = paramValue.split(',').map(function(projectionParam) {
16877								return {
16878									path: projectionParam
16879								};
16880							});
16881						} else {
16882							var paramKeyParts = paramKey.split(":");
16883							var path = paramKeyParts[0];
16884							var operator = paramKeyParts.length === 1 ? 'eq' : paramKeyParts[1].substring(1);
16885							var multiValue = toMultiValue(paramValue);
16886							var filter = {
16887								path: path,
16888								_path: path,
16889								operator: operator,
16890								_operator: operator,
16891								value: paramValue,
16892								_value: $utils.clone(paramValue),
16893								multiValue: multiValue,
16894								_multiValue: $utils.clone(multiValue)
16895							};
16896							addFilterTemplate($scope, filter);
16897							filters.push(filter);
16898						}
16899					}
16900				}
16901			});
16902		}
16903		return {
16904			filters: filters,
16905			limitEnabled: limitEnabled,
16906			limit: limit,
16907			orderEnabled: orderEnabled,
16908			orders: orders,
16909			projectionEnabled: projectionEnabled,
16910			projections: projections
16911		};
16912	}
16913
16914	function formatQuery(data) {
16915		var params = [];
16916		data.filters.forEach(function(filter) {
16917			if (filter.path && filter.operator && !$utils.isEmpty(filter.value)) {
16918				var operator = '';
16919				if (filter.operator !== 'eq') {
16920					operator = ':$' + filter.operator;
16921				}
16922				params.push(filter.path + operator + '=' + filter.value);
16923			}
16924		});
16925		if (data.limitEnabled && $utils.isNumeric(data.limit)) {
16926			params.push('_limit=' + data.limit);
16927		}
16928		if (data.orderEnabled) {
16929			var orders = [];
16930			data.orders.forEach(function(order) {
16931				if (order.path && order.direction) {
16932					orders.push(order.path + (order.direction === 'desc' ? ':desc' : ''));
16933				}
16934			});
16935			if (orders.length > 0) {
16936				params.push('_order=' + orders.join(','));
16937			}
16938		}
16939		if (data.projectionEnabled) {
16940			var projections = [];
16941			data.projections.forEach(function(projection) {
16942				if (projection.path) {
16943					projections.push(projection.path);
16944				}
16945			});
16946			if (projections.length > 0) {
16947				params.push('_projection=' + projections.join(','));
16948			}
16949		}
16950		if (params.length === 0) {
16951			return null;
16952		}
16953		return params.join('&');
16954	}
16955
16956	function addFilterTemplate($scope, filter) {
16957		var inOperator = filter.operator === 'in' || filter.operator === 'nin';
16958		var template = inOperator ? 'multi-text' : 'text';
16959		var columns = null;
16960		if (filter.operator === 'exists') {
16961			template = 'boolean';
16962			if (typeof filter.value !== 'boolean') {
16963				filter.value = (filter.value === true || filter.value === 'true');
16964			}
16965		} else if (filter.path && filter.operator !== 'like') {
16966			var column = getFilterColumn($scope, filter.path);
16967			if (column) {
16968				template = inOperator && column.type !== 'list' ? 'multi-column' : 'column';
16969				columns = [column];
16970			}
16971		}
16972		filter.template = template;
16973		filter.columns = columns;
16974	}
16975
16976	function toMultiValue(val) {
16977		if ($utils.isArray(val) && val.length > 0) {
16978			return val.map(function(v) {
16979				return {value: v};
16980			});
16981		}
16982		return [{}];
16983	}
16984
16985	function fromMultiValue(value) {
16986		if ($utils.isArray(value)) {
16987			return value.map(function(val) {
16988				return val.value;
16989			}).filter(function(val) {
16990				return !$utils.isEmpty(val);
16991			})
16992		}
16993		return null;
16994	}
16995
16996	function applyFiltersChanges($scope, filters) {
16997		filters.forEach(function(filter) {
16998			var changedFromCondition = filter.path !== filter._path ||
16999				filter.operator !== filter._operator;
17000			if (changedFromCondition) {
17001				filter._path = filter.path;
17002				filter._operator = filter.operator;
17003				addFilterTemplate($scope, filter);
17004			}
17005			if (!$utils.deepEquals(filter.value, filter._value)) {
17006				filter._value = $utils.clone(filter.value);
17007				filter.multiValue = toMultiValue(filter.value);
17008				filter._multiValue = $utils.clone(filter.multiValue);
17009			} else if (!$utils.deepEquals(filter.multiValue, filter._multiValue)) {
17010				filter._multiValue = $utils.clone(filter.multiValue);
17011				filter.value = fromMultiValue(filter.multiValue);
17012				filter._value = $utils.clone(filter.value);
17013			}
17014		});
17015	}
17016
17017	function getFilterColumn($scope, filterPath) {
17018		var col = $scope.columns[filterPath];
17019		if (col) {
17020			return col.column ? col.column[0] : null;
17021		}
17022		col = $entity.getColumnByPath($scope.entity, $scope.innerEntity, filterPath);
17023		if (col) {
17024			col = col.column;
17025			col = {
17026				name: 'value',
17027				type: col.type === 'formula' && col.formula_type ? col.formula_type : col.type,
17028				param: col.param,
17029				currency: col.currency,
17030				nolabel: true
17031			};
17032		}
17033		$scope.columns[filterPath] = {
17034			column: col ? [col] : null
17035		};
17036		return col;
17037	}
17038})();
17039
17040//// File panel-inner-entity-list-item.js ////
17041(function() {
17042
17043	$angular.controllers['panel-inner-entity-list-item'] = function($uibModalInstance, $scope, params) {
17044		$scope.close = function() {
17045			$uibModalInstance.dismiss();
17046		};
17047		if (!params.model[params.innerListColumn]) {
17048			params.model[params.innerListColumn] = [];
17049		}
17050		$scope.isNew = params.itemIndex === undefined;
17051		$scope.removeEnabled = params.removeEnabled;
17052		$scope.title = params.title;
17053		if ($scope.isNew) {
17054			$scope.model = params.itemData ? params.itemData : {};
17055			$scope.itemIndex = params.model[params.innerListColumn].length;
17056		} else {
17057			$scope.model = $utils.clone(params.model[params.innerListColumn][params.itemIndex]);
17058			$scope.itemIndex = params.itemIndex;
17059		}
17060		if (params.onChange) {
17061			$scope.$watch('model', params.onChange, true);
17062		}
17063
17064		$scope.meta = params.meta;
17065		var columns = params.columns;
17066		if (!columns) {
17067			columns = $entity.getInnerEntity(params.meta.id, params.innerEntity).columns;
17068		}
17069		$scope.columns = columns;
17070		$scope.ext = {
17071			extra: params.innerListColumn + '['+$scope.itemIndex+'].'
17072		};
17073		$scope.ientity = params.innerEntity;
17074
17075		$scope.save = function() {
17076			var item = $utils.clone($scope.model);
17077			if ($scope.isNew) {
17078				$entity.addColumnItem(params.model, params.innerListColumn, true, item);
17079			} else {
17080				params.model[params.innerListColumn][params.itemIndex] = item;
17081			}
17082
17083			$uibModalInstance.close(item);
17084		};
17085
17086		$scope.remove = function() {
17087			params.model[params.innerListColumn].splice(params.itemIndex, 1);
17088			$uibModalInstance.close(null);
17089		};
17090	}
17091})();
17092
17093//// File panel-wikipedia.js ////
17094(function() {
17095    
17096    
17097    $angular.controllers['panel-wikipedia'] = function($uibModalInstance, $scope, $translate, $http, params) {
17098		
17099    	$scope.editor = params.editor;
17100		
17101		$scope.wikiPage = null;
17102		$scope.value = "";//$scope.editor.html;
17103		$scope.wikiContent = "";
17104		
17105		// FINISH
17106		$scope.accept = function() {
17107			$scope.editor.insertContent($scope.value);
17108			$uibModalInstance.dismiss('success');
17109		};
17110		$scope.close = function() {
17111			$uibModalInstance.dismiss('cancel');
17112		};
17113		
17114		// WIKI SEARCH
17115		$scope.searchWikiPages = function(value) {
17116    		//console.log(value)
17117    		if ($utils.isBlank(value) || value.length < 3) {
17118    			return;
17119    		}
17120    		$rest.get({
17121    			url : contextPath + '/api/geo-new/'+$angular.contentScope.entity+'/a/searchWikiPages.json?name='+value,
17122    			loading : 'generic'
17123        	}).then(function(data) {
17124        		$scope.wikiPage = null;
17125        		$angular.rootScope.wikiPages = data.wikiPages;
17126        	});
17127    	};
17128        $scope.selectWikiPage = function(value) {
17129        	if ($utils.isBlank(value)) {
17130        		value = null;
17131        	}
17132        	$scope.wikiPage = value;
17133        };
17134        $scope.$watch('wikiPage', function(newValue, oldValue) {
17135        	//console.log('-> wikiPage: ' + newValue)
17136        	$scope.wp = newValue;
17137            if (newValue != oldValue) {
17138            	$scope.loadWikiData();
17139            }
17140        });
17141        $scope.loadWikiData = function() {
17142    		var url = $angular.rootScope.wikiPages[$scope.wikiPage];
17143    		if (url == null) {
17144    			$scope.wikiContent = "";
17145    			return;
17146    		}
17147			$rest.get({
17148    			url : contextPath + '/api/geo-new/'+$angular.contentScope.entity+'/a/loadWikiHtml.json?name='+url,
17149    			loading : 'generic'
17150        	}).then(function(data) {
17151        		$scope.wikiContent = data.description;
17152        	});
17153		};
17154		
17155		$scope.copySelection = function() {
17156			var txt = getSelectedText();
17157			if (!$utils.isBlank(txt)) {
17158				var old = $scope.value;
17159				if (!$utils.isBlank(old)) {
17160					old += "<br>";
17161				}
17162				old += txt;
17163				$scope.value = old;
17164			}
17165		};
17166		
17167		function getSelectedText() {
17168			var text = "";
17169			
17170			if (document.selection && document.selection.createRange) {
17171				var range = document.selection.createRange();
17172				text = range.htmlText;
17173			
17174			} else if (window.getSelection) {
17175				var selection = window.getSelection();
17176				if (selection.rangeCount > 0) {
17177					var range = selection.getRangeAt(0);
17178					var clonedSelection = range.cloneContents();
17179					var div = document.createElement('div');
17180					div.appendChild(clonedSelection);
17181					text = div.innerHTML;
17182				}
17183			}
17184			
17185//			 try {
17186//				if (window.getSelection) {
17187//					var nNd = document.createElement("any");
17188//					var w = window.getSelection().getRangeAt(0);
17189//					w.surroundContents(nNd);
17190//					text = nNd.innerHTML;
17191//				} else if (document.selection) {
17192//					var c = document.selection.createRange();
17193//					text = c.htmlText;
17194//				}
17195//			} catch (e) {
17196//				if (document.selection) {
17197//					text = document.selection.createRange();
17198//				} else {
17199//					text = window.getSelection();
17200//				}
17201//			}
17202			if (text != null) {
17203				text = text.toString().replace(/\[.*?\]/g, '');
17204			}
17205			
17206			// remove selection
17207//			if (window.getSelection) {
17208//				if (window.getSelection().empty) { // Chrome
17209//					window.getSelection().empty();
17210//				} else if (window.getSelection().removeAllRanges) { // Firefox
17211//					window.getSelection().removeAllRanges();
17212//				}
17213//			} else if (document.selection) { // IE?
17214//				document.selection.empty();
17215//			}
17216			
17217			return text;
17218		}
17219		
17220	}
17221	
17222})();
17223
17224//// File panel-form.js ////
17225(function() {
17226    
17227	$angular.controllers['panel-form'] = function($uibModalInstance, $scope, $translate, params, $timeout) {
17228		
17229		$scope.title = params.title;
17230		$scope.items = params.items;
17231		
17232		// init with default values
17233		$scope.model = {};
17234		if ($scope.items) {
17235			for (var k in $scope.items) {
17236				var item = $scope.items[k];
17237				if (item.type == 'radio') {
17238					if (item.selected) {
17239	        			$scope.model[item.name] = item.value;
17240					}
17241				} else {
17242					$scope.model[item.name] = item.value;
17243				}
17244			}
17245		}
17246		
17247		$scope.close = function() {
17248			$uibModalInstance.dismiss('cancel');
17249		};
17250	
17251		$scope.accept = function() {
17252			var result = params.successFunction($scope.model);
17253			if (result) {
17254				result.then(function() {
17255					$uibModalInstance.dismiss('success');
17256				});
17257			} else {
17258				$uibModalInstance.dismiss('success');
17259			}
17260		};
17261		
17262		$timeout(function() {
17263			$(".panel-form .modal-body .form-control")[0].focus();
17264		});
17265		
17266	}
17267})();
17268//// File panel-duration-calculator.js ////
17269(function() {
17270
17271	$angular.controllers['panel-duration-calculator'] = function($uibModalInstance, $scope, params) {
17272		$scope.timeFrom = params.time;
17273		$scope.timeTo = null;
17274		$scope.plusDays = 0;
17275		$scope.duration = params.duration && !isNaN(params.duration) ? Number(params.duration) : 0;
17276
17277		var time = parseTime($scope.timeFrom);
17278		if (time) {
17279			$scope.plusDays = Math.floor($scope.duration / 1440);
17280			var duration = $scope.duration % 1440;
17281			time.hours += Math.floor(duration / 60);
17282			time.minutes += duration % 60;
17283			if (time.minutes > 59) {
17284				time.minutes -= 60;
17285				time.hours++;
17286			}
17287			if (time.hours > 23) {
17288				time.hours -= 24;
17289			}
17290			$scope.timeTo = formatTime(time);
17291		}
17292
17293		$scope.$watch('timeFrom', recalculate);
17294		$scope.$watch('timeTo', recalculate);
17295		$scope.$watch('plusDays', recalculate);
17296
17297		$scope.close = function() {
17298			$uibModalInstance.dismiss('cancel');
17299		};
17300
17301		$scope.save = function() {
17302			$uibModalInstance.dismiss('success');
17303			params.successFunction($scope.duration);
17304		};
17305
17306		function recalculate() {
17307			var from = parseTime($scope.timeFrom);
17308			var to = parseTime($scope.timeTo);
17309			if (from && to) {
17310				var hours = to.hours - from.hours;
17311				var minutes = to.minutes - from.minutes;
17312				if (minutes < 0) {
17313					minutes += 60;
17314					hours--;
17315				}
17316				if (hours < 0) {
17317					hours += 24;
17318				}
17319				$scope.duration = minutes + (hours * 60) + ($scope.plusDays * 1440);
17320			}
17321		}
17322	}
17323
17324	function parseTime(time) {
17325		if (!time || time.length !== 5) {
17326			return null;
17327		}
17328		var parts = time.split(':');
17329		if (parts.length !== 2 || isNaN(parts[0]) || isNaN(parts[1])) {
17330			return null;
17331		}
17332		var hour = Number(parts[0]);
17333		var minutes = Number(parts[1]);
17334		if (hour < 0 || hour > 23 || minutes < 0 || minutes > 59) {
17335			return null;
17336		}
17337		return {
17338			hours: hour,
17339			minutes: minutes
17340		}
17341	}
17342
17343	function formatTime(time) {
17344		var text = time.hours < 10 ? '0' : '';
17345		text += time.hours;
17346		text += ':';
17347		text += time.minutes < 10 ? '0' : '';
17348		text += time.minutes;
17349		return text;
17350	}
17351
17352})();
17353
17354//// File panel-columns.js ////
17355(function() {
17356
17357	$angular.controllers['panel-columns'] = function($uibModalInstance, $scope, $translate, params) {
17358
17359		$scope.close = function() {
17360			$uibModalInstance.dismiss();
17361		};
17362
17363		$scope.clickButton = function(result) {
17364			$uibModalInstance.close(result);
17365		};
17366
17367		$scope.title = params.title;
17368		$scope.meta = params.meta;
17369		$scope.columns = params.columns;
17370		$scope.model = params.model;
17371		$scope.opts = params.opts;
17372		$scope.buttons = params.buttons;
17373	};
17374})();
17375
17376//// File panel-translate.js ////
17377(function() {
17378    
17379    
17380    $angular.controllers['panel-translate'] = function($uibModalInstance, $scope, $translate, $http, params, $sce) {
17381		
17382		$scope.url = contextPath + "/api/translate.html?lang=" + params.lang;
17383		
17384		$scope.close = function() {
17385			$uibModalInstance.dismiss('cancel');
17386		};
17387		
17388		$angular.initTranslate = function() {
17389			var text = $sce.trustAsHtml(params.text);
17390			$('#itranslate').contents().find("#translation").html(text);
17391		};
17392		
17393		$scope.accept = function() {
17394			var text;
17395			if (params.html) {
17396				text = $('#itranslate').contents().find("#translation").html();
17397			} else {
17398				text = $('#itranslate').contents().find("#translation").text();
17399			}
17400			var lang = $('#itranslate').contents().find("select.goog-te-combo").val();
17401			
17402			if ($utils.isBlank(text)) {
17403				return;
17404			}
17405			
17406			$uibModalInstance.dismiss('success');
17407			params.successFunction({ text:text, lang:lang });
17408		};
17409		
17410	}
17411	
17412})();
17413
17414//// File panel-formula-builder.js ////
17415(function() {
17416	$angular.controllers['panel-formula-builder'] = function($uibModalInstance, $scope, params, debounce) {
17417		$scope.functions = Object.keys(formulas);
17418		$scope.formulas = formulas;
17419		$scope.entity = params.entity;
17420		$scope.innerEntity = params.innerEntity;
17421		$scope.model = parseFormula(params.formula);
17422
17423		$scope.$watch('model', debounce(function(data) {
17424			$scope.formula = formatFormula(data);
17425		}, 500), true);
17426
17427		$scope.close = function() {
17428			$uibModalInstance.dismiss();
17429		};
17430
17431		$scope.finish = function() {
17432			$scope.formula = formatFormula($scope.model);
17433			$uibModalInstance.close($scope.formula);
17434		};
17435
17436		$scope.clear = function() {
17437			$scope.model = parseFormula();
17438			prepareDefaults($scope);
17439		};
17440
17441		$scope.onChangeFunc = function() {
17442			$scope.model.args = prepareArgs($scope.model.args, $scope.model.func);
17443		};
17444
17445		$scope.buildArgFormula = function(arg) {
17446			$angular.openModalLocal('panel-formula-builder', {
17447				size: 'xl',
17448				entity: $scope.entity,
17449				formula: arg.value
17450			}).then(function(result) {
17451				arg.value = result;
17452			});
17453		};
17454
17455		$scope.addArg = function(arg) {
17456			var newArg = $utils.clone(arg);
17457			newArg.value = null;
17458			newArg.optional = true;
17459			$scope.model.args.push(newArg);
17460		};
17461
17462		prepareDefaults($scope);
17463		$scope.formula = formatFormula($scope.model);
17464	};
17465
17466	function prepareDefaults($scope) {
17467
17468	}
17469
17470	function parseFormula(value) {
17471		if (value) {
17472			value = value.trim();
17473			if ($utils.isFormula(value)) {
17474				var indexOfStart = value.indexOf('(');
17475				var func = value.substring(0, indexOfStart);
17476				var data = value.substring(indexOfStart + 1, value.length - 1);
17477				var args = splitArgs(data)
17478					.map(parseArg)
17479					.filter($utils.isNotEmpty);
17480				return {
17481					func: func,
17482					args: prepareArgs(args, func)
17483				};
17484			}
17485		}
17486		return null;
17487	}
17488
17489	function parseArg(value) {
17490		if (value) {
17491			value = value.trim();
17492			var type = 'value';
17493			if ($utils.isFormula(value)) {
17494				type = 'formula';
17495			} else if (isQuotedString(value)) {
17496				type = 'text';
17497				value = value.substring(1, value.length - 1);
17498			}
17499			return {
17500				valueType: type,
17501				value: value
17502			};
17503		}
17504		return null;
17505	}
17506
17507	function isQuotedString(text) {
17508		if (text.length === 0) {
17509			return false;
17510		}
17511		var first = text[0];
17512		if (first === '"' || first === "'") {
17513			var end = findEndOfString(text, 0, first);
17514			if (end === text.length - 1) {
17515				return true;
17516			}
17517		}
17518		return false;
17519	}
17520
17521	function splitArgs(content) {
17522		var args = [];
17523		var preArg = '';
17524		var index = 0;
17525		while (index < content.length) {
17526			var character = content[index];
17527			if (character === '"' || character === "'") {
17528				var endOfString = findEndOfString(content, index, character);
17529				if (endOfString > content.length) {
17530					args.push(preArg + content.substring(index, content.length));
17531					return args;
17532				}
17533				preArg += content.substring(index, endOfString + 1);
17534				index = endOfString;
17535			} else if (character === '(') {
17536				var endOfParenthesis = findEndOfParenthesis(content, index);
17537				if (endOfParenthesis > content.length) {
17538					args.push(preArg + content.substring(index, content.length));
17539					return args;
17540				}
17541				preArg += content.substring(index, endOfParenthesis + 1);
17542				index = endOfParenthesis;
17543			} else if (character === ',') {
17544				args.push(preArg);
17545				preArg = '';
17546			} else {
17547				preArg += character;
17548			}
17549			index++;
17550		}
17551		if (preArg.length > 0) {
17552			args.push(preArg);
17553		}
17554		return args;
17555	}
17556
17557	function findEndOfString(content, start, quote) {
17558		var index = start + 1;
17559		while (index < content.length) {
17560			var character = content[index];
17561			if (character === quote) {
17562				return index;
17563			} else if (character === '\\') {
17564				index++;
17565			}
17566			index++;
17567		}
17568		return index;
17569	}
17570
17571	function findEndOfParenthesis(content, start) {
17572		var index = start + 1;
17573		var opened = 0;
17574		while (index < content.length) {
17575			var character = content[index];
17576			if (character === ')') {
17577				if (opened === 0) {
17578					return index;
17579				}
17580				opened--;
17581			} else if (character === '(') {
17582				opened++;
17583			} else if (character === '"' || character === "'") {
17584				index = findEndOfString(content, index, character);
17585			}
17586			index++;
17587		}
17588		return index;
17589	}
17590
17591	function prepareArgs(fromArgs, func) {
17592		if (!fromArgs) {
17593			fromArgs = [];
17594		}
17595		var formula = formulas[func];
17596		if (!formula) {
17597			return fromArgs;
17598		}
17599		var args = [];
17600		var iterFromArgs = 0;
17601		for (var iter = 0; iter < formula.args.length; iter++) {
17602			var formulaArg = formula.args[iter];
17603			var fromArg = iterFromArgs < fromArgs.length ? fromArgs[iterFromArgs] : null;
17604			if (formulaArg.multiple) {
17605				var numMultipleArgs = 0;
17606				for (iterFromArgs; iterFromArgs < fromArgs.length; iterFromArgs++) {
17607					var arg = $utils.clone(formulaArg);
17608					arg.value = fromArgs[iterFromArgs].value;
17609					if (formulaArg.min && numMultipleArgs >= formulaArg.min) {
17610						arg.optional = true;
17611					}
17612					args.push(arg);
17613					numMultipleArgs++;
17614				}
17615				if (formulaArg.min) {
17616					for (var i = numMultipleArgs; i < formulaArg.min; i++) {
17617						args.push($utils.clone(formulaArg));
17618					}
17619				}
17620			} else {
17621				var argument = $utils.clone(formulaArg);
17622				argument.value = fromArg ? fromArg.value : null;
17623				args.push(argument);
17624				iterFromArgs++;
17625			}
17626		}
17627		return args;
17628	}
17629
17630	function formatFormula(data) {
17631		if (data && data.func && formulas[data.func]) {
17632			return data.func + '(' + data.args
17633				.filter(function(arg) {
17634					return $utils.isNotEmpty(arg.value);
17635				})
17636				.map(function(arg) {
17637					return arg.type === 'text' ? "'" + arg.value + "'" : arg.value;
17638				})
17639				.join(', ') + ')';
17640		}
17641		return null;
17642	}
17643
17644	var formulas = {
17645		display: {
17646			args: [{name: 'path'}]
17647		},
17648		math: {
17649			args: [{name: 'operation', label: 'msg.formula.arg.operation'},
17650				{name: 'decimals', type: 'number', optional: true, label: 'msg.formula.arg.decimals'}]
17651		},
17652		string: {
17653			args: [{name: 'path'}]
17654		},
17655		number: {
17656			args: [{name: 'path'},
17657				{name: 'decimals', type: 'number', optional: true, label: 'msg.formula.arg.decimals'}]
17658		},
17659		bool: {
17660			args: [{name: 'path'}]
17661		},
17662		not: {
17663			args: [{name: 'path'}]
17664		},
17665		and: {
17666			args: [{name: 'path', multiple: true, min: 1}]
17667		},
17668		or: {
17669			args: [{name: 'path', multiple: true, min: 1}]
17670		},
17671		eq: {
17672			args: [{name: 'path'},
17673				{name: 'path'}]
17674		},
17675		ne: {
17676			args: [{name: 'path'},
17677				{name: 'path'}]
17678		},
17679		gt: {
17680			args: [{name: 'path'},
17681				{name: 'path'}]
17682		},
17683		gte: {
17684			args: [{name: 'path'},
17685				{name: 'path'}]
17686		},
17687		lt: {
17688			args: [{name: 'path'},
17689				{name: 'path'}]
17690		},
17691		lte: {
17692			args: [{name: 'path'},
17693				{name: 'path'}]
17694		},
17695		empty: {
17696			args: [{name: 'path'}]
17697		},
17698		length: {
17699			args: [{name: 'path'}]
17700		},
17701		replace: {
17702			args: [{name: 'path'},
17703				{name: 'search text', type: 'text'},
17704				{name: 'replace by', type: 'text', optional: true}]
17705		},
17706		substring: {
17707			args: [{name: 'path'},
17708				{name: 'from index', type: 'number'},
17709				{name: 'to index', type: 'number'}]
17710		},
17711		round: {
17712			args: [{name: 'path'},
17713				{name: 'decimals', type: 'number', optional: true, label: 'msg.formula.arg.decimals'}]
17714		},
17715		sum: {
17716			args: [{name: 'path'},
17717				{name: 'decimals', type: 'number', optional: true, label: 'msg.formula.arg.decimals'}]
17718		},
17719		avg: {
17720			args: [{name: 'path'},
17721				{name: 'decimals', type: 'number', optional: true, label: 'msg.formula.arg.decimals'}]
17722		},
17723		if: {
17724			args: [{name: 'condition'},
17725				{name: 'result'},
17726				{name: 'else', optional: true}]
17727		},
17728		convert_to_text: {
17729			args: [{name: 'number'},
17730				{name: 'language', optional: true},
17731				{name: 'currency', optional: true}]
17732		}
17733	};
17734})();
17735
17736//// File panel-column-types.js ////
17737(function() {
17738    
17739    
17740    $angular.controllers['panel-column-types'] = function($uibModalInstance, $scope, $translate, $http, params) {
17741		
17742		$scope.column = params.column;
17743		
17744		$scope.key_search = "";
17745		
17746		$scope.close = function() {
17747			$uibModalInstance.dismiss('cancel');
17748		}
17749
17750		$scope.select = function(type) {
17751			$scope.column.type = type;
17752			$uibModalInstance.dismiss('success');
17753		};
17754		
17755		$scope.showColumnType = function(type) {
17756			if ($utils.isBlank($scope.key_search)) {
17757				return true;
17758			}
17759			var txt = $utils.columnTypeText(type).toLowerCase();
17760			return txt.contains($scope.key_search.toLowerCase());
17761		}
17762		
17763	}
17764	
17765})();
17766
17767//// File panel-change-history.js ////
17768(function() {
17769	
17770	$angular.controllers['panel-change-history'] = function($uibModalInstance, $scope, params) {
17771		$scope.entity = params.entity;
17772		$scope.entityId = params.entityId;
17773		$scope.changes = [];
17774		$scope.limit = 0;
17775		$scope.index = 0;
17776
17777		$scope.load = function() {
17778			$rest.get({
17779				skipHideLoading: true,
17780				url: contextPath + '/api/changes.json?entity=' + $scope.entity + '&entity_id=' + $scope.entityId + '&index=' + $scope.index,
17781				loading: 'generic'
17782			}).then(function(o) {
17783				if ($scope.limit == 0) {
17784					$scope.limit = o.list.length;
17785				}
17786				$scope.changes = o.list;
17787			});
17788		};
17789
17790		$scope.next = function() {
17791			if ($scope.changes.length === $scope.limit) {
17792				$scope.index += $scope.limit;
17793				$scope.load();
17794			}
17795		};
17796
17797		$scope.prev = function() {
17798			if ($scope.index > 0) {
17799				$scope.index = Math.max(0, $scope.index - $scope.limit);
17800				$scope.load();
17801			}
17802		};
17803
17804		$scope.load();
17805
17806		$scope.close = function() {
17807			$uibModalInstance.dismiss('cancel');
17808		};
17809	}
17810})();
17811
17812//// File panel-licenses.js ////
17813(function() {
17814    
17815    
17816    $angular.controllers['panel-licenses'] = function($uibModalInstance, $rootScope, $scope, $translate, $http, params) {
17817	
17818		$scope.key = params.key;
17819		$scope.showButtons = params.showButtons; 
17820		
17821		$scope.close = function() {
17822			$uibModalInstance.dismiss('cancel');
17823		}
17824		
17825		$scope.model = {};
17826    	$rest.get({
17827    		skipHideLoading: true,
17828			url: contextPath + "/www/license/"+params.key+".json",
17829			loading: "generic"
17830		}).then(function(o) {
17831			$scope.model = o;
17832		});
17833		
17834		$scope.accept = function() {
17835			$angular.acceptLicense(params.key, null, function() {
17836				$utils.floatingMsg($translate.instant("status.accepted"));
17837    			$scope.close();
17838			});	
17839		};
17840		$scope.reject = function() {
17841			$angular.rejectLicense(params.key, null, function() {
17842				$utils.floatingMsg($translate.instant("status.rejected"));
17843    			$scope.close();
17844			});
17845		};
17846		
17847		
17848	}
17849	
17850})();
17851
17852//// File panel-action-info.js ////
17853(function() {
17854	
17855	$angular.controllers['panel-action-info'] = function($uibModalInstance, $scope, params) {
17856		$scope.title = params.title;
17857		$scope.action = params.action;
17858
17859		$scope.close = function() {
17860			$uibModalInstance.dismiss('cancel');
17861		};
17862
17863		$scope.buildAddToGoogleCalendarLink = function() {
17864			var action = $scope.action;
17865			var dateFrom = $utils.printDate(action.date, 'yyyyMMddTHHmm');
17866			var dateTo = $utils.printDate(action.date + 3600000, 'yyyyMMddTHHmm');
17867			return 'https://www.google.com/calendar/render' +
17868				'?action=TEMPLATE' +
17869				'&text=' + encodeURIComponent($scope.title) +
17870				'&dates=' + dateFrom + '00/' + dateTo + '00';
17871		};
17872
17873		$scope.buildAddToOutlookCalendarLink = function() {
17874			var action = $scope.action;
17875			var date = $utils.printDate(action.date, 'yyyy-MM-ddTHH:mm');
17876			return 'https://outlook.live.com/calendar/0/deeplink/compose' +
17877				'?path=/calendar/action/compose' +
17878				'&rru=addevent' +
17879				'&startdt=' + date + ':00' +
17880				'&subject=' + encodeURIComponent($scope.title);
17881		};
17882	}
17883})();
17884
17885//// File panel-add-entity.js ////
17886(function() {
17887
17888	$angular.controllers['panel-add-entity'] = function($uibModalInstance, $scope, $translate, $http, params) {
17889		$scope.close = function() {
17890			$uibModalInstance.dismiss();
17891		};
17892		$scope.entity = params.entity;
17893		$scope.model = {};
17894		$angular.panelAddEntityScope = $scope;
17895
17896		$entity.getEntityForce($scope.entity, function(meta) {
17897			$rest.get({
17898				skipHideLoading: true,
17899				url: contextPath + "/api/geo-new/" + $scope.entity + "/new.json",
17900				loading: "generic"
17901			}).then(function(o) {
17902				$scope.geoForm = $utils.setFormviews(null, o).find(function(fv) {
17903					return !fv.form.optional;
17904				});
17905
17906				$scope.opts = {};
17907				var baseColumns = meta.columns;
17908				if ($scope.geoForm) {
17909					$scope.columns = $scope.geoForm.form.columns;
17910					baseColumns = $scope.geoForm.form.columns;
17911				} else if ($entity.isCreateSimple($scope.entity)) {
17912					$scope.columns = [$entity.getDefaultColumn(meta)];
17913				} else {
17914					$scope.columns = baseColumns;
17915				}
17916
17917				// auto fill columns
17918				if (baseColumns && $angular.contentScope) {
17919					var pScope = $angular.contentScope;
17920					for (var key in baseColumns) {
17921						var col = baseColumns[key];
17922						if (col.type === 'entity' && col.param === pScope.entity && pScope.entityId) {
17923							$scope.model[col.name] = pScope.entityId;
17924							col.unexposed = true; // hide relation
17925
17926						} else if (col.type === 'list' && col.param === pScope.entity && pScope.entityId) {
17927							if ($scope.model[col.name] == null) {
17928								$scope.model[col.name] = [];
17929							}
17930							$scope.model[col.name].push(pScope.entityId);
17931							col.unexposed = true; // hide relation
17932
17933						} else if (col.type === 'formula') {
17934							col.unexposed = true; // hide formulas
17935						}
17936					}
17937				}
17938
17939				$scope.meta = meta;
17940			});
17941		});
17942
17943		var formCallback;
17944		$scope.save = function(callback) {
17945			var submit = $("#geoAddEntityForm input[type=submit]");
17946			if (submit.size() > 0) {
17947				formCallback = callback;
17948				setTimeout(function(){
17949					submit.click(); // to validate form
17950				}, 100);
17951			}
17952		};
17953		
17954		$scope.submit = function(e, form) {
17955			$utils.preventDefault(e);
17956				
17957			if (!$utils.validateForm(form)) {
17958				return;
17959			}
17960			
17961			var data = $utils.clone($scope.model);
17962			if ($scope.geoForm) {
17963				data = {
17964					formId: $scope.geoForm.form.id,
17965					data: data
17966				}
17967			}
17968			$rest.post({
17969				loading: "generic",
17970				url: contextPath + "/api/geo-new/" + $scope.entity + ".json",
17971				data: data
17972			}).then(function(createdObject) {
17973				$instance.checkHasToRefresh($scope.entity);
17974				$entity.getEntityForce($scope.entity, function() {
17975					$utils.postSave(data, createdObject, true, formCallback, function(callback) {
17976						$uibModalInstance.close(createdObject);
17977						if (callback) {
17978							callback();
17979						}
17980					});	
17981				});
17982			});
17983		};
17984	}
17985	
17986})();
17987
17988//// File panel-move.js ////
17989(function() {
17990	
17991	$angular.controllers['panel-move'] = function($uibModalInstance, $scope) {
17992		$scope.close = function() {
17993			$uibModalInstance.dismiss('cancel');
17994		};
17995		$scope.model = {};
17996		$scope.move = function(entity) {
17997			if ($utils.isBlank(entity)) {
17998				return;
17999			}
18000			var url;
18001			if ($utils.isBlank($angular.contentScope.entityId)) {
18002				url = contextPath + "/api/geo/" + $angular.contentScope.entity + "/a/move.json" + $paginator.getQueryStringAll() + "&target=" + entity;
18003			} else {
18004				url = contextPath + "/api/geo/" + $angular.contentScope.entity + "/" + $angular.contentScope.entityId + "/move.json?target=" + entity;
18005			}
18006			$rest.put({
18007				url: url,
18008				loading: "generic",
18009				successMessage: $angular.get("$translate").instant('msg.moved')
18010			}).then(function() {
18011				$uibModalInstance.dismiss('cancel');
18012				if ($utils.isBlank($angular.contentScope.entityId)) {
18013					$angular.contentScope.refresh();
18014				} else {
18015					$events.back();
18016				}
18017			});
18018		};
18019	}
18020	
18021})();
18022
18023//// File panel-prompt.js ////
18024(function() {
18025
18026	$angular.controllers['panel-prompt'] = function($uibModalInstance, $scope, $translate, params) {
18027		$scope.title = params.title;
18028		$scope.label = params.label;
18029
18030		$scope.close = function() {
18031			$uibModalInstance.dismiss();
18032		};
18033
18034		$scope.accept = function() {
18035			if (params.validate) {
18036				var result = params.validate($scope.value);
18037				if (result !== true) {
18038					if (typeof result === 'string') {
18039						$utils.floatingError($translate.instant(result));
18040					}
18041					return;
18042				}
18043			}
18044			$uibModalInstance.close($scope.value);
18045		};
18046	}
18047})();
18048
18049//// File panel-edit-json.js ////
18050(function() {
18051
18052	$angular.controllers['panel-edit-json'] = function($uibModalInstance, $scope, params) {
18053		if (typeof params.json === 'string') {
18054			$scope.json = params.json;
18055		} else {
18056			$scope.json = angular.toJson(params.json, 2);
18057		}
18058
18059		$scope.close = function() {
18060			$uibModalInstance.dismiss('cancel');
18061		};
18062
18063		$scope.finishEdition = function() {
18064			try {
18065				params.successFunction(angular.fromJson($scope.json));
18066			} catch (e) {
18067				alert('Json invalid');
18068				console.error(e);
18069				return;
18070			}
18071			$uibModalInstance.dismiss('success');
18072		};
18073	}
18074})();
18075
18076//// File panel-video.js ////
18077(function() {
18078    
18079    
18080    $angular.controllers['panel-video'] = function($uibModalInstance, $scope, $translate, $http, params) {
18081		
18082		$scope.videoUrl = params.video;
18083		
18084		$scope.close = function() {
18085			$uibModalInstance.dismiss('cancel');
18086		}
18087		
18088	}
18089	
18090})();
18091
18092//// File panel-login.js ////
18093(function() {
18094
18095	$angular.controllers['panel-login'] = function($uibModalInstance, $scope) {
18096		$scope.close = function() {
18097			$uibModalInstance.dismiss();
18098		};
18099		$scope.finish = function() {
18100			$uibModalInstance.close();
18101		};
18102	}
18103})();
18104
18105//// File panel-confirm.js ////
18106(function() {
18107    
18108    
18109    $angular.controllers['panel-confirm'] = function($uibModalInstance, $scope, $translate, $http, params) {
18110		
18111		$scope.title = params.title;
18112		if (params.text && params.text != null) {
18113			$scope.text = params.text;
18114		} else {
18115			$scope.text = $translate.instant("msg.confirm");
18116		}
18117		
18118		$scope.close = function() {
18119			$uibModalInstance.dismiss('cancel');
18120			
18121			if (params.cancelFunction) {
18122				params.cancelFunction();
18123			}
18124		}
18125
18126		$scope.accept = function() {
18127			$uibModalInstance.dismiss('success');
18128			params.successFunction();
18129		}
18130		
18131	}
18132	
18133})();
18134
18135//// File panel-json.js ////
18136(function() {
18137    "use strict";
18138    
18139    $angular.controllers['panel-json'] = function($uibModalInstance, $scope, $translate, $http, params, $sce) {
18140		
18141		$scope.content = $utils.sortKeys(params.json);
18142		//console.dir(params.content)
18143		
18144		$scope.close = function() {
18145			$uibModalInstance.dismiss('cancel');
18146		}
18147		
18148	};
18149	
18150})();
18151
18152//// File datamixer-user-inputs.component.js ////
18153(function() {
18154
18155	$angular.controllers['datamixer-user-inputs-component'] = function($scope) {
18156
18157		$scope.$on('initDatamixerUserInputsComponent', function(event, config) {
18158			$scope.datamixer = config.model;
18159			$scope.model = {
18160				columns: prepareColumns(),
18161				sample: prepareSample()
18162			}
18163			$scope.$watch('model.sample', onChangeSample, true);
18164		});
18165
18166		$scope.$on('refreshDatamixerUserInputsComponent', function(event, data) {
18167			$scope.datamixer = data.model;
18168			$scope.model.columns = prepareColumns();
18169			$scope.model.sample = prepareSample();
18170		});
18171
18172		function prepareColumns() {
18173			if ($scope.datamixer.user_inputs && $scope.datamixer.user_inputs.columns) {
18174				return angular.fromJson($scope.datamixer.user_inputs.columns);
18175			}
18176			return [];
18177		}
18178
18179		function prepareSample() {
18180			if ($scope.datamixer.user_inputs && $scope.datamixer.user_inputs.sample_data) {
18181				return angular.fromJson($scope.datamixer.user_inputs.sample_data);
18182			}
18183			return {};
18184		}
18185
18186		$scope.onChange = function(columns) {
18187			if (!$scope.datamixer.user_inputs) {
18188				$scope.datamixer.user_inputs = {};
18189			}
18190			$scope.datamixer.user_inputs.columns = angular.toJson(columns);
18191		};
18192
18193		function onChangeSample(sample) {
18194			if (!$scope.datamixer.user_inputs) {
18195				$scope.datamixer.user_inputs = {};
18196			}
18197			$scope.datamixer.user_inputs.sample_data = angular.toJson(sample);
18198		}
18199	}
18200})();
18201
18202//// File booking.component.js ////
18203(function() {
18204
18205	$angular.components['booking'] = {
18206		templateUrl: 'booking.component.html',
18207		bindings: {
18208			geoObject: '<',
18209			model: '<',
18210			hideTitle: '<',
18211			hideBookButton: '<',
18212			onChange: '&'
18213		},
18214		controller: function($scope, $timeout) {
18215			var self = $angular.prepareComponent(this, $scope, $timeout);
18216
18217			self.$onChanges = function() {
18218				$scope.refresh();
18219			};
18220
18221			$scope.refresh = function() {
18222				$scope.geoObject = self.geoObject;
18223				$scope.showTitle = !self.hideTitle;
18224				$scope.showBookButton = !self.hideBookButton;
18225				$scope.onChange = self.onChange;
18226
18227				var booking = self.model;
18228				if (!booking) {
18229					booking = {};
18230				}
18231				$scope.model = booking;
18232
18233				var scheduleConfig = $scope.geoObject.geo_schedule;
18234				var bookingConfig = scheduleConfig.booking;
18235
18236				$scope.availableDays = $scope.getAvailableDates(scheduleConfig);
18237
18238				if (!$scope.bookingEntityColumns) {
18239					$rest.get({
18240						skipHideLoading: true,
18241						loading: "generic",
18242						url: contextPath + "/www/booking/a/columns.json?entity=" + $scope.geoObject.entity + "&entity
18242Id=" + $scope.geoObject.id
18243					}).then(function(bookingEntity) {
18244						var bookingEntityColumns = bookingEntity.columns;
18245						$scope.bookingEntityColumns = bookingEntityColumns;
18246						var extra = {};
18247						if (bookingEntityColumns && bookingEntityColumns.length > 0) {
18248							$scope.extraDataEntity = bookingEntity;
18249							for (var iter = 0; iter < bookingEntityColumns.length; iter++) {
18250								var name = bookingEntityColumns[iter].name;
18251								extra[name] = null;
18252								if (booking.extra_data) {
18253									$scope.geoObject[name] = booking.extra_data[name];
18254								}
18255							}
18256						}
18257						$scope.extra = extra;
18258					});
18259				}
18260
18261				$scope.single = bookingConfig.capacity === 1 || bookingConfig.capacity_per_booking === 1;
18262
18263				$scope.booking = {
18264					day: booking ? $utils.parseDay(booking.day) : null,
18265					time: booking ? booking.time : null,
18266					quantity: booking ? booking.quantity : null
18267				};
18268
18269				$scope.$watch('booking', $scope.validateIfBookingIsReady, true);
18270				$scope.$watch('geoObject', $scope.validateIfBookingIsReady, true);
18271				$scope.validateIfBookingIsReady();
18272			};
18273
18274			$scope.getAvailableDates = function(geoSchedule) {
18275				var today = new Date();
18276				today.setHours(0);
18277				today.setMinutes(0);
18278				today.setSeconds(0);
18279				today.setMilliseconds(0);
18280				
18281				var now = today.getTime() - 1;
18282				var list = $scope.getBookingDates(geoSchedule)
18283					.map(function(date) {
18284						return $utils.parseDay(date.day);
18285					});
18286				return list.filter(function(day, position) {
18287					return list.indexOf(day) === position;
18288				}).filter(function(day) {
18289					return new Date(day).getTime() - now > 0;
18290				});
18291			};
18292
18293			$scope.getBookingTimes = function(geoSchedule, day) {
18294				if (day) {
18295					var parseDay = $utils.toISODay(day);
18296					var list = $scope.getBookingDates(geoSchedule)
18297						.filter(function(date) {
18298							return parseDay === date.day;
18299						})
18300						.map(function(date) {
18301							return date.time_from;
18302						});
18303					if (list.length == 1) {
18304						$scope.booking.time = list[0];
18305					}	
18306					return list;
18307				}
18308				return [];
18309			}
18310
18311			$scope.getBookingDates = function(geoSchedule) {
18312				if (geoSchedule == null || geoSchedule.dates == null) {
18313					return [];
18314				}
18315				var dates = geoSchedule.dates;
18316				var maxBookings = geoSchedule.booking.max_bookings;
18317				if (maxBookings) {
18318					dates = dates.filter(function(date) {
18319						return date && (!date.num_bookings || date.num_bookings < maxBookings);
18320					});
18321				}
18322				var capacity = geoSchedule.booking.capacity;
18323				if (capacity) {
18324					dates = dates.filter(function(date) {
18325						return date && (!date.booked || date.booked < capacity);
18326					});
18327				}
18328				return dates;
18329			}
18330
18331			$scope.validateIfBookingIsReady = function() {
18332				var day = $scope.booking.day;
18333				var time = $scope.booking.time;
18334				var quantity = $scope.single ? 1 : $scope.booking.quantity;
18335
18336				$scope.isReady = !!(day && time && quantity);
18337				$scope.model.entity = $scope.geoObject.entity;
18338				$scope.model.entity_id = $scope.geoObject.id;
18339				$scope.model.day = day ? $utils.toISODay(day) : null;
18340				$scope.model.time = time;
18341				$scope.model.quantity = quantity;
18342
18343				var extraData = {};
18344				for (var extra in $scope.extra) {
18345					extraData[extra] = $scope.geoObject[extra];
18346				}
18347				$scope.model.extra_data = extraData;
18348
18349				if ($scope.onChange) {
18350					$scope.onChange({
18351						booking: $scope.model,
18352						isReady: $scope.isReady
18353					});
18354				}
18355			};
18356
18357			$scope.book = function() {
18358				if (!$user.isLogged() && !$instance.isLoginDisabled()) {
18359					$user.openLoginAsPanel(function() {
18360						$scope.book();
18361					});
18362					return;
18363				}
18364				var form = $angular.searchInScope("bookingForm");
18365				if (!$utils.validateForm(form)) {
18366					return;
18367				}
18368				if (!$scope.isReady) {
18369					$utils.floatingError($angular.get("$translate").instant("fields.required"));
18370					return;
18371				}
18372				$rest.post({
18373					url: contextPath + '/www/booking.json',
18374					data: $scope.model,
18375					loading: 'generic'
18376				}).then(function(o) {
18377					$scope.booking.success = true;
18378					$scope.booking.day = null;
18379					$scope.booking.time = null;
18380					$scope.booking.quantity = null;
18381					
18382					if ($entity.isGeoBuy(o.object.entity)) {
18383						$angular.get("$location").path($instance.getBaseUrl() + "booking/" + o.id + "/pay");
18384					}
18385				});
18386			};
18387		}
18388	};
18389})();
18390
18391//// File datamixer-processors.component.js ////
18392(function() {
18393
18394	var FUNCTIONS = [
18395		{name: 'count', from: ['list', 'string']},
18396		{name: 'sum', from: ['list']},
18397		{name: 'filter', from: ['list']},
18398		{name: 'query', from: ['list']},
18399		{name: 'group', from: ['list']},
18400		{name: 'sort', from: ['list']},
18401		{name: 'get', from: ['object', 'list', 'string']},
18402		{name: 'replace', from: ['list']},
18403		{name: 'add', from: ['day', 'time', 'object', 'list', 'integer', 'decimal', 'string']},
18404		{name: 'subtract', from: ['day', 'time', 'object', 'list', 'integer', 'decimal', 'string']},
18405		{name: 'multiply', from: ['list', 'integer', 'decimal']},
18406		{name: 'divide', from: ['list', 'integer', 'decimal']},
18407		{name: 'fetch', from: ['list', 'string']},
18408		{name: 'clone', from: ['day', 'time', 'object', 'list', 'integer', 'decimal', 'string', 'boolean']},
18409		{name: 'wrap', from: ['day', 'time', 'object', 'list', 'integer', 'decimal', 'string', 'boolean']},
18410		{name: 'unwrap', from: ['list']},
18411		{name: 'formula', from: ['object', 'list']}
18412	];
18413
18414	var maxDemoObjects = null;
18415
18416	$angular.controllers['datamixer-processors-component'] = function($scope) {
18417
18418		$scope.$on('initDatamixerProcessorsComponent', function(event, config) {
18419			$scope.model = {};
18420			$scope.datamixer = config.model;
18421			prepareToEdit();
18422			$scope.addDataFrom = {};
18423		});
18424
18425		$scope.$on('refreshDatamixerProcessorsComponent', function(event, data) {
18426			$scope.datamixer = data.model;
18427			prepareToEdit();
18428		});
18429
18430		$scope.printFrom = printFrom;
18431
18432		function prepareToEdit() {
18433			maxDemoObjects = $scope.datamixer.max_demo_objects;
18434			$scope.model.variables = [];
18435			if ($scope.datamixer.user_inputs) {
18436				if ($scope.datamixer.user_inputs.columns) {
18437					var inputs = angular.fromJson($scope.datamixer.user_inputs.columns);
18438					if ($utils.isArray(inputs)) {
18439						inputs.forEach(function(input) {
18440							if (input.name) {
18441								$scope.model.variables.push(input.name);
18442							}
18443						});
18444					}
18445				}
18446				if ($scope.datamixer.user_inputs.sample_data) {
18447					$scope.model.inputData = angular.fromJson($scope.datamixer.user_inputs.sample_data);
18448				}
18449			}
18450			$scope.model.samples = [];
18451			$scope.model.processors = $scope.datamixer.data_processors ? $scope.datamixer.data_processors.map(function(d) {
18452				if (d.new_var) {
18453					$scope.model.variables.push(d.new_var);
18454				}
18455				return {
18456					id: d.id,
18457					from: d.from,
18458					new_var: d.new_var,
18459					operations: d.operations ? angular.fromJson(d.operations) : [],
18460					built_by_component: d.built_by_component
18461				};
18462			}) : [];
18463			$scope.model.variables.push('today');
18464			var numProcessors = $scope.model.processors.length;
18465			prepareSample(0, numProcessors);
18466		}
18467
18468		function prepareToSave() {
18469			$scope.datamixer.data_processors = $scope.model.processors.map(function(d) {
18470				return {
18471					id: d.id,
18472					from: d.from,
18473					new_var: d.new_var,
18474					operations: d.operations ? angular.toJson(d.operations) : null,
18475					built_by_component: d.built_by_component
18476				};
18477			});
18478		}
18479
18480		function saveDatamixer() {
18481			prepareToSave();
18482			$angular.contentScope.submitGeoEdit();
18483		}
18484
18485		function prepareSample(index, numProcessors) {
18486			if (index >= numProcessors) {
18487				return;
18488			}
18489			var processors = [];
18490			for (var iter = 0; iter < index; iter++) {
18491				processors.push($scope.model.processors[iter]);
18492			}
18493			var processorToSample = $scope.model.processors[index];
18494			processors.push({
18495				from: processorToSample.from,
18496				operations: processorToSample.operations,
18497				new_var: '__$sample$__'
18498			});
18499			getSample(processors, $scope.model.inputData, index).then(function(result) {
18500				$scope.model.samples[index] = result.sample;
18501				prepareSample(index + 1, numProcessors);
18502			});
18503		}
18504
18505		$scope.addDataFromEntity = function() {
18506			if ($scope.addDataFrom.entity) {
18507				var entityId = $scope.addDataFrom.entity;
18508				$angular.openModalLocal('panel-datamixer-operation', {
18509					model: $scope.model,
18510					size: 'xl',
18511					isNewData: true,
18512					prevProcessors: $scope.model.processors,
18513					from: entityId
18514				}).then(function(result) {
18515					var processor = $entity.addColumnItem($scope.model, 'processors', true, {
18516						from: entityId,
18517						new_var: result.newVar
18518					});
18519					if (result.operation) {
18520						$entity.addColumnItem(processor, 'operations', false, result.operation);
18521					}
18522					saveDatamixer();
18523				});
18524			}
18525			$scope.addDataFrom = {};
18526		};
18527
18528		$scope.addDataFromVar = function() {
18529			var variable = $scope.addDataFrom.variable;
18530			if (variable) {
18531				var from = '$' + variable;
18532				$angular.openModalLocal('panel-datamixer-operation', {
18533					model: $scope.model,
18534					size: 'xl',
18535					isNewData: true,
18536					prevProcessors: $scope.model.processors,
18537					from: from
18538				}).then(function(result) {
18539					var processor = $entity.addColumnItem($scope.model, 'processors', true, {
18540						from: from,
18541						new_var: result.newVar
18542					});
18543					if (result.operation) {
18544						$entity.addColumnItem(processor, 'operations', false, result.operation);
18545					}
18546					saveDatamixer();
18547				});
18548			}
18549			$scope.addDataFrom = {};
18550		};
18551
18552		$scope.removeData = function(processor, index) {
18553			$rest.openConfirmPanel('msg.datamixer.removeData', null, function() {
18554				$scope.model.processors.splice(index, 1);
18555				saveDatamixer();
18556			});
18557		}
18558
18559		$scope.moveDataUp = function(processor, index) {
18560			var aux = $scope.model.processors[index].id;
18561			$scope.model.processors[index].id = $scope.model.processors[index - 1].id;
18562			$scope.model.processors[index - 1].id = aux;
18563			saveDatamixer();
18564		};
18565
18566		$scope.moveDataDown = function(processor, index) {
18567			var aux = $scope.model.processors[index].id;
18568			$scope.model.processors[index].id = $scope.model.processors[index + 1].id;
18569			$scope.model.processors[index + 1].id = aux;
18570			saveDatamixer();
18571		};
18572
18573		$scope.initDatamixerOperationsList = function(scope, processor) {
18574			initDatamixerOperationsList(scope, $scope.model, processor, saveDatamixer, function() {
18575				var processors = [];
18576				for (var i = 0; i < $scope.model.processors.length; i++) {
18577					var iter = $scope.model.processors[i];
18578					if (iter.new_var === processor.new_var) {
18579						break;
18580					}
18581					processors.push(iter);
18582				}
18583				return {
18584					prevProcessors: processors,
18585					from: processor.from
18586				};
18587			});
18588		};
18589	}
18590
18591	$angular.controllers['panel-datamixer-operation'] = function($uibModalInstance, $scope, $translate, params) {
18592		$scope.model = params.model;
18593		$scope.prevProcessors = params.prevProcessors;
18594		$scope.from = params.from;
18595		$scope.operation = params.operation ? $utils.clone(params.operation) : {};
18596		$scope.autoPathFields = [];
18597		$scope.filterOperators = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte'];
18598		$scope.isExpression = $scope.operation && $scope.operation.params && $scope.operation.params.value && (typeof $scope.operation.params.value === 'object');
18599
18600		$scope.close = function() {
18601			$uibModalInstance.dismiss();
18602		};
18603
18604		$scope.save = function() {
18605			if ($scope.operation && $scope.operation.function) {
18606				var operation = $scope.operation;
18607				getSampleOperation(operation).then(function(sampleResult) {
18608					operation.from_type = {type: $scope.fromType};
18609					operation.result_type = sampleResult.result_type;
18610					saveOperation(operation);
18611				});
18612			} else {
18613				saveOperation(null);
18614			}
18615		};
18616
18617		function saveOperation(operation) {
18618			if (params.isNewData) {
18619				$angular.openModal('panel-prompt', {
18620					title: 'msg.datamixer.storeData',
18621					label: 'msg.datamixer.newVariable',
18622					validate: validateNewVar
18623				}).then(function(newVar) {
18624					$uibModalInstance.close({
18625						newVar: newVar,
18626						operation: operation
18627					});
18628				});
18629			} else {
18630				$uibModalInstance.close({
18631					operation: operation
18632				});
18633			}
18634		}
18635
18636		function validateNewVar(varName) {
18637			if (!varName) {
18638				return false;
18639			}
18640			if ($scope.model.variables.indexOf(varName) >= 0) {
18641				return 'msg.datamixer.variableAlreadyExists';
18642			}
18643			if (/^[a-zA-Z0-9_]+$/.test(varName)) {
18644				return true;
18645			}
18646			return 'msg.datamixer.invalidVariableName';
18647		}
18648
18649		$scope.processData = function() {
18650			getSampleOperation($scope.operation).then(function(result) {
18651				$scope.toData = result.sample;
18652			});
18653		};
18654
18655		$scope.getAvailableFunctions = function() {
18656			if ($scope.fromType) {
18657				return FUNCTIONS.filter(function(func) {
18658					return func.from.indexOf($scope.fromType) >= 0;
18659				}).map(function(func) {
18660					return func.name;
18661				});
18662			}
18663			return [];
18664		};
18665
18666		$scope.getFunctionText = function(func, fromType) {
18667			var messageKeys = [
18668				'msg.datamixer.operation.' + func + '.' + fromType,
18669				'msg.datamixer.operation.' + func
18670			];
18671			return $utils.tryToTranslate($translate, messageKeys);
18672		};
18673
18674		function getSampleOperation(operation) {
18675			var processors = $scope.prevProcessors.slice();
18676			processors.push({
18677				from: $scope.from,
18678				operations: operation ? [operation] : [],
18679				new_var: '__$sample$__'
18680			});
18681			return getSample(processors, $scope.model.inputData);
18682		}
18683
18684		function addFieldsForAutoPath(fields, data, prefix) {
18685			if ($utils.isArray(data)) {
18686				data.forEach(function(item) {
18687					addFieldsForAutoPath(fields, item, prefix);
18688				});
18689			} else if ($utils.isObject(data)) {
18690				$utils.keys(data).forEach(function(key) {
18691					var field = prefix + key;
18692					if (fields.indexOf(field) < 0) {
18693						fields.push(field);
18694						addFieldsForAutoPath(fields, data[key], field + '.');
18695					}
18696				});
18697			}
18698		}
18699		getSampleOperation(null).then(function(result) {
18700			$scope.fromData = result.sample;
18701			$scope.fromType = result.result_type.type;
18702			$scope.fromType = !$scope.fromType || $scope.fromType === 'null' ? 'list' : $scope.fromType;
18703			var getSubType = function(t) {
18704				return t.subtype ? getSubType(t.subtype) : t.type;
18705			}
18706			$scope.fromSubType = getSubType(result.result_type);
18707			$scope.fromSubType = !$scope.fromSubType || $scope.fromSubType === 'null' ? 'object' : $scope.fromSubType;
18708			var fields = [];
18709			var data = result.data;
18710			addFieldsForAutoPath(fields, data, '');
18711			$scope.autoPathFields = fields;
18712			var obj = null;
18713			if ($utils.isArray(data) && data.length > 0) {
18714				obj = data[0];
18715			} else if ($utils.isObject(data)) {
18716				obj = data;
18717			}
18718			if (obj) {
18719				$scope.fromDataEntity = obj.entity;
18720			}
18721		});
18722
18723		$scope.buildQuery = function() {
18724			if (!$scope.operation.params) {
18725				$scope.operation.params = {};
18726			}
18727			$angular.openModalLocal('panel-query-builder', {
18728				size: 'xl',
18729				entity: $scope.fromDataEntity,
18730				query: $scope.operation.params.query
18731			}).then(function(result) {
18732				$scope.operation.params.query = result;
18733			});
18734		};
18735
18736		$scope.changeIsExpression = function() {
18737			$scope.isExpression = !$scope.isExpression;
18738			if ($scope.isExpression) {
18739				$scope.operation.params.value = {operations: []};
18740			} else {
18741				$scope.operation.params.value = null;
18742			}
18743		};
18744
18745		$scope.initDatamixerOperationsList = function(scope, obj, attribute) {
18746			var processor = obj[attribute];
18747			if (!processor || typeof processor !== 'object') {
18748				processor = {
18749					operations: []
18750				};
18751				obj[attribute] = processor;
18752			}
18753			initDatamixerOperationsList(scope, $scope.model, processor, null, function() {
18754				var processors = $scope.prevProcessors.slice();
18755				var from = processor.from;
18756				if (from === '$' || from.startsWith('$.')) {
18757					var operations = [];
18758					operations.push({
18759						function: 'get',
18760						params: {
18761							at: 0
18762						}
18763					});
18764					if (from.startsWith('$.') && from.length > 2) {
18765						operations.push({
18766							function: 'get',
18767							params: {
18768								at: from.substring(2)
18769							}
18770						});
18771					}
18772					var newVar = '__$var' + Math.random() + '$__';
18773					processors.push({
18774						from: $scope.from,
18775						operations: operations,
18776						new_var: newVar
18777					});
18778					from = '$' + newVar;
18779				}
18780				return {
18781					prevProcessors: processors,
18782					from: from
18783				};
18784			});
18785		};
18786	}
18787
18788	function getSample(processors, inputData, index) {
18789		return $rest.put({
18790			url: contextPath + '/api/dashboard/' + (index ? index : 0) + '/sample.json',
18791			data: {
18792				max_demo_objects: maxDemoObjects,
18793				data_processors: processors,
18794				input: inputData
18795			}
18796		}).then(function(data) {
18797			var sample = data.sample ? angular.toJson(data.sample, 2) : data.sample;
18798			var max = 100000;
18799			if (sample && sample.length > max) {
18800				sample = sample.substring(0, max) + '...';
18801			}
18802			return {
18803				data: data.sample,
18804				sample: sample,
18805				result_type: data.result_type
18806			};
18807		});
18808	}
18809
18810	function printFrom(from) {
18811		var $translate = $angular.get('$translate');
18812		if (from === '$') {
18813			return $translate.instant('msg.datamixer.fromItem');
18814		}
18815		if (from.startsWith('$.')) {
18816			return $translate.instant('msg.datamixer.fromItemPath', {from: from.substring(2)});
18817		}
18818		if (from.startsWith('$')) {
18819			return $translate.instant('msg.datamixer.fromVariable', {from: from.substring(1)});
18820		}
18821		return $translate.instant('msg.datamixer.fromEntity', {from: $constants.getEntityDesc('entity', from)});
18822	}
18823
18824	function initDatamixerOperationsList($scope, model, processor, saveDatamixer, prevProcessorsGetter) {
18825		$scope.processor = processor;
18826		$scope.canEditAsJson = !!saveDatamixer;
18827
18828		$scope.printFrom = printFrom;
18829		$scope.printOperation = function(operation) {
18830			var $translate = $angular.get('$translate');
18831			var params = operation.params;
18832			var func = operation.function;
18833			var messageKeys = [];
18834			var text = '...';
18835			if (operation.from_type) {
18836				var fromType = operation.from_type.type;
18837				var fromSubtype = operation.from_type.subtype;
18838				if (fromSubtype) {
18839					messageKeys.push('msg.datamixer.operation.' + func + '.' + fromType + '.' + fromSubtype.type + '.params');
18840				}
18841				messageKeys.push('msg.datamixer.operation.' + func + '.' + fromType + '.params');
18842				messageKeys.push('msg.datamixer.operation.' + func + '.params');
18843				if (fromSubtype) {
18844					messageKeys.push('msg.datamixer.operation.' + func + '.' + fromType + '.' + fromSubtype.type);
18845				}
18846				messageKeys.push('msg.datamixer.operation.' + func + '.' + fromType);
18847				messageKeys.push('msg.datamixer.operation.' + func);
18848				var operatorText = (params && params.operator) ? $translate.instant('msg.operator.' + params.operator) : null;
18849				var filterValue = (params && params.value && typeof params.value !== 'object') ? params.value : null;
18850				text = $utils.tryToTranslate($translate, messageKeys, {
18851					params: params,
18852					operator: operatorText,
18853					filterValue: filterValue
18854				});
18855				if (!text) {
18856					text = $translate.instant('msg.datamixer.operation.invalid', operation);
18857				}
18858			}
18859			text = '<p>
18859' + text + '</p>';
18860			var innerProcessor = null;
18861			if (params && params.from) {
18862				innerProcessor = params;
18863			} else if (params && params.value && params.value.from) {
18864				innerProcessor = params.value;
18865			}
18866			if (innerProcessor) {
18867				var innerText = '<div class="row mt-2"><div class="col">' + $scope.printFrom(innerProcessor.from) + '</div></div>';
18868				if (innerProcessor.operations) {
18869					innerText += innerProcessor.operations.map(function(op) {
18870						return '<div class="row ml-2"><div class="col-12"><hr /></div><div class="col">' + $scope.printOperation(op) + '</div></div>';
18871					}).join('');
18872				}
18873				text += '<div class="ml-4">' + innerText + '</div>';
18874			}
18875			return text;
18876		};
18877
18878		$scope.addOperation = function() {
18879			var prev = prevProcessorsGetter();
18880			var prevProcessors = prev.prevProcessors;
18881			var processor = $scope.processor;
18882			var from = prev.from;
18883			if (processor.operations) {
18884				var extraProcessor = {
18885					from: from,
18886					operations: processor.operations.slice(),
18887					new_var: '__$var' + Math.random() + '$__'
18888				};
18889				prevProcessors.push(extraProcessor);
18890				from = '$' + extraProcessor.new_var;
18891			}
18892			$angular.openModalLocal('panel-datamixer-operation', {
18893				model: model,
18894				size: 'xl',
18895				isNewData: false,
18896				prevProcessors: prevProcessors,
18897				from: from
18898			}).then(function(result) {
18899				if (result.operation) {
18900					$entity.addColumnItem(processor, 'operations', false, result.operation);
18901				}
18902				if (saveDatamixer) {
18903					saveDatamixer();
18904				}
18905			});
18906		};
18907
18908		$scope.editOperation = function(index) {
18909			var prev = prevProcessorsGetter();
18910			var prevProcessors = prev.prevProcessors;
18911			var processor = $scope.processor;
18912			var from = prev.from;
18913			if (processor.operations) {
18914				var extraProcessor = {
18915					from: from,
18916					operations: processor.operations.slice(0, index),
18917					new_var: '__$var' + Math.random() + '$__'
18918				};
18919				prevProcessors.push(extraProcessor);
18920				from = '$' + extraProcessor.new_var;
18921			}
18922			$angular.openModalLocal('panel-datamixer-operation', {
18923				model: model,
18924				size: 'xl',
18925				isNewData: false,
18926				operation: processor.operations[index],
18927				prevProcessors: prevProcessors,
18928				from: from
18929			}).then(function(result) {
18930				if (result.operation) {
18931					processor.operations[index] = result.operation;
18932				} else {
18933					processor.operations.splice(index, 1);
18934				}
18935				if (saveDatamixer) {
18936					saveDatamixer();
18937				}
18938			});
18939		};
18940
18941		$scope.removeOperation = function(index) {
18942			$rest.openConfirmPanel('msg.datamixer.removeOperation', null, function() {
18943				$scope.processor.operations.splice(index, 1);
18944				if (saveDatamixer) {
18945					saveDatamixer();
18946				}
18947			});
18948		};
18949
18950		$scope.editOperationsAsJson = function() {
18951			var simplifyToEdit = function(data) {
18952				if ($utils.isArray(data)) {
18953					return data.map(simplifyToEdit);
18954				} else if ($utils.isObject(data)) {
18955					var newObj = {};
18956					for (var key in data) {
18957						if (key !== 'from_type' && key !== 'result_type') {
18958							newObj[key] = simplifyToEdit(data[key]);
18959						}
18960					}
18961					return newObj;
18962				}
18963				return data;
18964			};
18965			var simpleJson = simplifyToEdit($scope.processor.operations);
18966			$angular.openModal('panel-edit-json', {
18967				json: simpleJson,
18968				size: 'lg',
18969				successFunction: function(json) {
18970					$scope.processor.operations = json;
18971					saveDatamixer();
18972				}
18973			});
18974		};
18975	}
18976})();
18977
18978//// File geo-users.js ////
18979(function() {
18980
18981	$angular.controllers['geo-users-component'] = function($scope, $element) {
18982
18983		function prepareGeoUsers() {
18984			var $scope = $angular.contentScope;
18985	
18986			var fromDate = new Date();
18987			fromDate.setDate(fromDate.getDate() - 15);
18988			$scope.nearbyUsers = {
18989				list: [],
18990				filter: {
18991					from: $utils.fromDate(fromDate, $constants.formatDay),
18992					to: null,
18993					distance: 100,
18994					time: 60
18995				}
18996			};
18997	
18998			$scope.searchNearbyUsers = function() {
18999				var params = [];
19000				var filter = $scope.nearbyUsers.filter;
19001				if (filter.from) {
19002					params.push('from=' + $utils.toISODay(filter.from));
19003				}
19004				if (filter.to) {
19005					params.push('to=' + $utils.toISODay(filter.to));
19006				}
19007				if (filter.distance) {
19008					params.push('maxMetersOfDistance=' + filter.distance);
19009				}
19010				if (filter.time) {
19011					params.push('maxMinutesOfDifference=' + filter.time);
19012				}
19013				$rest.get({
19014					url: contextPath + '/api/nearby-users/' + $scope.entityId + '.json?' + params.join('&'),
19015					loading: 'generic'
19016				}).then(function(o) {
19017					$scope.nearbyUsers.list = o;
19018				});
19019			};
19020	
19021			$scope.searchNearbyUsers();
19022		}
19023	
19024		prepareGeoUsers();
19025		
19026	}
19027})();
19028//// File dashboard.component.js ////
19029(function() {
19030
19031	$angular.components['dashboard'] = {
19032		templateUrl: 'dashboard.component.html',
19033		bindings: {
19034			datamixer: '<',
19035			inputData: '<',
19036			onDatamixerReceived: '&',
19037			onInputDataChanged: '&'
19038		},
19039		controller: function($scope, $timeout, $translate, $httpParamSerializer, $interval) {
19040			var self = $angular.prepareComponent(this, $scope, $timeout);
19041
19042			self.$onChanges = function() {
19043				$scope.refresh();
19044			};
19045
19046			$scope.refresh = function() {
19047				$scope.inputData = self.inputData ? self.inputData : {};
19048				initDashboard();
19049				$scope.$watch('inputData', function() {
19050					self.onInputDataChanged({inputData: $scope.inputData});
19051					initDashboard();
19052				}, true);
19053
19054			};
19055
19056			$scope.reload = function() {
19057				lastInputData = null;
19058				initDashboard();
19059			};
19060
19061			$interval(function() {
19062				if (!isLoading && $scope.secondsToReload && $scope.secondsToReload > 0) {
19063					$scope.secondsToReload--;
19064					if ($scope.secondsToReload === 0) {
19065						$scope.reload();
19066					}
19067				}
19068			}, 1000);
19069
19070			var lastInputData = null;
19071			var isLoading = true;
19072			function initDashboard() {
19073				if (lastInputData !== null && $utils.isEqual(lastInputData, $scope.inputData)) {
19074					return;
19075				}
19076				isLoading = true;
19077				var qs = $httpParamSerializer($scope.inputData);
19078				$rest.get({
19079					url: contextPath + '/api/dashboard/' + self.datamixer + '.json?' + qs,
19080					loading: 'generic'
19081				}).then(function(o) {
19082					$angular.setTitle($utils.parse(o.name));
19083					$scope.secondsToReload = o.datamixer.reload_every_seconds;
19084					self.onDatamixerReceived({datamixerData: o.datamixer});
19085					if (o.safeemployee) {
19086						$scope.components = buildSafeemployeeComponents(o.safeemployee);
19087					} else {
19088						$scope.components = o.components;
19089					}
19090					$scope.inputData = o.input ? o.input : {};
19091					lastInputData = $utils.clone($scope.inputData);
19092					if (o.datamixer.user_inputs && o.datamixer.user_inputs.columns) {
19093						$scope.inputs = angular.fromJson(o.datamixer.user_inputs.columns);
19094					}
19095					isLoading = false;
19096					$scope.componentsDownloadLinks = prepareComponentDownloadLinks();
19097				});
19098			}
19099
19100			function prepareComponentDownloadLinks() {
19101				var links = {};
19102				if ($scope.components == null) {
19103					return links;
19104				}
19105				$scope.components.forEach(function(component) {
19106					if (component.type === 'table') {
19107						var id = self.datamixer + '-' + component.id;
19108						var qs = $httpParamSerializer(self.inputData);
19109						links[component.id] = [{
19110							icon: 'cloud_download',
19111							href: contextPath + '/api/dashboard/' + id + '/downloadComponent.xlsx?' + qs + '&geo_instance=' + $instance.getId()
19112						}];
19113					}
19114				});
19115				return links;
19116			}
19117
19118			function buildSafeemployeeComponents(data) {
19119				return [{
19120					type: 'counter',
19121					title: $translate.instant('msg.inRisk'),
19122					icon: 'sanitizer',
19123					color: '#C7C41B',
19124					value: data.people_in_risk,
19125					width: 3
19126				}, {
19127					type: 'counter',
19128					title: $translate.instant('msg.suspicious'),
19129					icon: 'local_hospital',
19130					color: '#FFA500',
19131					value: data.people_suspicious,
19132					width: 3
19133				}, {
19134					type: 'counter',
19135					title: $translate.instant('msg.sick'),
19136					icon: 'sick',
19137					color: '#E01B43',
19138					value: data.people_sick,
19139					width: 3
19140				}, {
19141					type: 'counter',
19142					title: $translate.instant('msg.recovered'),
19143					icon: 'thumb_up',
19144					color: '#008000',
19145					value: data.people_recovered,
19146					width: 3
19147				}, {
19148					type: 'map',
19149					title: $translate.instant('msg.dashboard.map.title'),
19150					subtitle: $translate.instant('msg.dashboard.map.description'),
19151					markers: data.locations,
19152					width: 6
19153				}, {
19154					type: 'chart',
19155					title: $translate.instant('msg.dashboard.graph.title'),
19156					subtitle: $translate.instant('msg.dashboard.graph.description'),
19157					chart: buildSafeemployeeChart(data),
19158					width: 6
19159				}];
19160			}
19161
19162			function buildSafeemployeeChart(data) {
19163				var series = [];
19164				var colors = [];
19165				if (data.people_suspicious != null) {
19166					series.push({
19167						name: $translate.instant('msg.suspicious'),
19168						data: data.cases_by_date.map(function(x) {
19169							return x.people_suspicious;
19170						})
19171					});
19172					colors.push('#FFA500');
19173				}
19174				if (data.people_sick != null) {
19175					series.push({
19176						name: $translate.instant('msg.sick'),
19177						data: data.cases_by_date.map(function(x) {
19178							return x.people_sick;
19179						})
19180					});
19181					colors.push('#E01B43');
19182				}
19183				if (data.people_recovered != null) {
19184					series.push({
19185						name: $translate.instant('msg.recovered'),
19186						data: data.cases_by_date.map(function(x) {
19187							return x.people_recovered;
19188						})
19189					});
19190					colors.push('#008000');
19191				}
19192				return {
19193					chart: {
19194						type: 'bar'
19195					},
19196					series: series,
19197					colors: colors,
19198					responsive: [{
19199						breakpoint: 480,
19200						options: {
19201							legend: {
19202								position: 'bottom',
19203								offsetX: -10,
19204								offsetY: 0
19205							}
19206						}
19207					}],
19208					plotOptions: {
19209						bar: {
19210							horizontal: false
19211						}
19212					},
19213					xaxis: {
19214						type: 'datetime',
19215						categories: data.cases_by_date.map(function(x) {
19216							return x.date;
19217						})
19218					},
19219					fill: {
19220						opacity: 1
19221					}
19222				};
19223			}
19224		}
19225	};
19226})();
19227
19228//// File input-radio.js ////
19229(function() {
19230	
19231	$angular.components['inputRadio'] = {
19232		templateUrl: "input-radio.html",
19233		bindings: {
19234			key: '@', model: '=', list: '=', changed: '&'
19235		},
19236		controller: function() {
19237		}
19238	}
19239	
19240})();
19241//// File dashboard-card.component.js ////
19242(function() {
19243
19244	$angular.components['dashboardCard'] = {
19245		templateUrl: 'dashboard-card.component.html',
19246		bindings: {
19247			component: '<',
19248			type: '@',
19249			value: '<',
19250			chart: '<',
19251			markers: '<',
19252			tableColumns: '<',
19253			tableData: '<',
19254			downloadLinks: '<',
19255			hoverIcon: '@'
19256		},
19257		controller: function($scope, $timeout, $element) {
19258			var self = $angular.prepareComponent(this, $scope, $timeout);
19259
19260			self.$onChanges = function() {
19261				$scope.prepare();
19262			};
19263
19264			$scope.refresh = function() {
19265				$scope.$watch('$ctrl.component', $scope.prepare, true);
19266			};
19267
19268			$scope.prepare = function() {
19269				$scope.downloadLinks = self.downloadLinks ? self.downloadLinks : [];
19270				$scope.component = $utils.clone(self.component);
19271				if (self.type) {
19272					$scope.component.type = self.type;
19273				}
19274				if (!$utils.isBlank(self.value)) {
19275					$scope.component.value = self.value;
19276				}
19277				if (self.markers) {
19278					$scope.component.markers = self.markers;
19279				}
19280				if (self.tableColumns) {
19281					$scope.component.table_columns = self.tableColumns;
19282				}
19283				if (self.tableData) {
19284					$scope.component.data_list = self.tableData;
19285				}
19286				if ($scope.component.type === 'chart') {
19287					var chart = self.chart ? self.chart : $scope.component.chart;
19288					$timeout(function() {
19289						buildChart(chart);
19290					});
19291				}
19292				$scope.hoverIcon = self.hoverIcon;
19293			};
19294
19295			$scope.getCardViewStyle = function(component) {
19296				var style = {};
19297				if (component.color) {
19298					style['border-left'] = '5px solid ' + component.color;
19299				}
19300				return style;
19301			};
19302
19303			$scope.getCardTitleStyle = function(component) {
19304				var style = {};
19305				if (component.color) {
19306					style['color'] = component.color;
19307				}
19308				return style;
19309			};
19310
19311			function buildChart(chart) {
19312				if ($scope.apexChart) {
19313					$scope.apexChart.destroy();
19314				}
19315				if (!chart || !chart.chart) {
19316					return;
19317				}
19318				var chartParsed = parseChartFunctions(chart);
19319				var $el = $element.find('.dashboard-card-chart');
19320				$scope.apexChart = new ApexCharts($el[0], chartParsed);
19321				$scope.apexChart.render().catch(function() {
19322					$el.empty().append('<div class="alert alert-warning">Invalid data</div>');
19323				});
19324			}
19325
19326			function parseChartFunctions(chart) {
19327				if (chart === null) {
19328					return 0;
19329				}
19330				if ($utils.isArray(chart)) {
19331					return chart.map(function(item) {
19332						return parseChartFunctions(item);
19333					});
19334				}
19335				if ($utils.isObject(chart)) {
19336					var newObj = {};
19337					for (var key in chart) {
19338						newObj[key] = parseChartFunctions(chart[key]);
19339					}
19340					return newObj;
19341				}
19342				if (typeof chart === 'string') {
19343					if (chart === '__false__') {
19344						return false;
19345					}
19346					if (chart.trim().startsWith('function(')) {
19347						return new Function('return ' + chart)(); // FIXME unsafe-eval
19348					}
19349				}
19350				return chart;
19351			}
19352			
19353		}
19354	};
19355})();
19356
19357//// File columns-form.component.js ////
19358(function() {
19359
19360	$angular.components['columnsForm'] = {
19361		templateUrl: 'columns-form.component.html',
19362		bindings: {
19363			entity: '<',
19364			model: '<',
19365			submitEvent: '@',
19366			wrapWithForm: '<',
19367			readonly: '<',
19368			allInOneColumn: '<',
19369			onChange: '&',
19370			onSubmit: '&'
19371		},
19372		controller: function($scope, $timeout) {
19373			var self = $angular.prepareComponent(this, $scope, $timeout);
19374
19375			self.$onChanges = function() {
19376				$scope.refresh();
19377			};
19378
19379			$scope.refresh = function() {
19380				$scope.formSubmitId = 'form-component-' + $utils.getRandom();
19381				$scope.onChange = self.onChange;
19382				$scope.onSubmit = self.onSubmit;
19383				$scope.wrapWithForm = !!self.wrapWithForm;
19384
19385				var entity = self.entity;
19386
19387				if (self.submitEvent && $scope.wrapWithForm) {
19388					$scope.$on(self.submitEvent, $scope.validateForm);
19389				}
19390
19391				if (!self.model) {
19392					self.model = {};
19393				}
19394				$scope.model = self.model;
19395
19396				var columns = entity.columns;
19397				var formColumns = [];
19398				$scope.requiredColumns = [];
19399				if (columns) {
19400					formColumns = columns.filter(function(column) {
19401						return !column.hidden && column.updatable;
19402					});
19403					$scope.requiredColumns = formColumns.filter(function(column) {
19404						return column.required;
19405					}).map(function(column) {
19406						return column.name;
19407					});
19408				}
19409
19410				$scope.config = {
19411					meta: entity,
19412					columns: formColumns,
19413					model: $scope.model,
19414					opts: {
19415						readonly: self.readonly,
19416						long: self.allInOneColumn
19417					}
19418				};
19419
19420				$scope.$watch('model', $scope.validateIfFormIsReady, true);
19421				$scope.validateIfFormIsReady();
19422			};
19423
19424			$scope.validateIfFormIsReady = function() {
19425				$scope.isReady = $scope.requiredColumns.every(function(required) {
19426					return $scope.model[required];
19427				});
19428				if ($scope.onChange) {
19429					$scope.onChange({
19430						data: $scope.model,
19431						isReady: $scope.isReady
19432					});
19433				}
19434			};
19435
19436			$scope.validateForm = function() {
19437				$timeout(function() {
19438					$('#' + $scope.formSubmitId).click();
19439				}, 0, false);
19440			};
19441
19442			$scope.submitForm = function() {
19443				if ($scope.onSubmit) {
19444					$scope.onSubmit({
19445						data: $scope.model,
19446						isReady: $scope.isReady
19447					});
19448				}
19449			};
19450		}
19451	};
19452})();
19453
19454//// File select-geo.component.js ////
19455(function() {
19456
19457	var LIMIT = 50;
19458
19459	$angular.components['selectGeo'] = {
19460		templateUrl: 'select-geo.component.html',
19461		require: {
19462			ngModelCtrl: 'ngModel'
19463		},
19464		bindings: {
19465			ngModel: '<',
19466			entity: '<',
19467			query: '<',
19468			allowClear: '<',
19469			emptyOption: '<',
19470			multiple: '<',
19471			linkSelected: '<',
19472			numToShow: '<',
19473			placeholder: '@',
19474			omitInstances: '<',
19475			onChange: '&'
19476		},
19477		controller: function($scope, $timeout, $translate, $element) {
19478			var self = $angular.prepareComponent(this, $scope, $timeout);
19479
19480			self.$onChanges = function() {
19481				$scope.refresh();
19482			};
19483
19484			var scrollBind = false;
19485			$scope.model = {};
19486			$scope.optionsBySearch = {};
19487
19488			$scope.refresh = function() {
19489				$scope.entity = self.entity;
19490				$scope.allowClear = $utils.isTrue(self.allowClear);
19491				$scope.omitInstances = $utils.isTrue(self.omitInstances);
19492				$scope.multiple = $utils.isTrue(self.multiple);
19493				$scope.placeholder = self.placeholder ? self.placeholder : $translate.instant('msg.select.search');
19494				$scope.linked = self.linkSelected === undefined ? !$constants.isWeb : $utils.isTrue(self.linkSelected);
19495				$scope.numToShow = self.numToShow === undefined ? 10 : self.numToShow;
19496				setModels(self.ngModel);
19497				loadOptions(false);
19498			};
19499
19500			$scope.getName = function(optionId) {
19501				if (!optionId) {
19502					return null;
19503				}
19504				if (optionId === $constants.blank) {
19505					return $constants.blank;
19506				}
19507				return $constants.getEntityDesc(self.entity, optionId);
19508			};
19509
19510			$scope.isDisabled = function(optionId) {
19511				if (!optionId) {
19512					return null;
19513				}
19514				if (optionId === $constants.blank) {
19515					return false;
19516				}
19517				if (self.entity === 'entity' && $instance.current && $instance.current.entityList) {
19518					var entity = $instance.current.entityList.find(function(entity) {
19519						return entity.id === optionId;
19520					});
19521					if (entity) {
19522						return entity.enabled !== undefined && !entity.enabled;
19523					}
19524				}
19525				var enabled = $constants.isEnabled(self.entity, optionId);
19526				if (enabled === undefined) {
19527					return undefined;
19528				}
19529				return !enabled;
19530			};
19531
19532			$scope.searchOptions = function(search) {
19533				$scope.model.search = search;
19534				loadOptions(false);
19535			};
19536
19537			$scope.onSelect = function($select) {
19538				var itemId = $select.selected;
19539
19540				if ($scope.multiple) {
19541					$scope.model.list.push(itemId);
19542					$scope.model.id = null;
19543					self.ngModelCtrl.$setViewValue($scope.model.list);
19544				} else {
19545					self.ngModelCtrl.$setViewValue(itemId);
19546				}
19547
19548				$select.close();
19549				self.onChange({
19550					$select: $select
19551				});
19552
19553				loadOptions(false);
19554			};
19555
19556			$scope.removeMultipleSelectedOption = function(e, itemId) {
19557				$utils.preventDefault(e);
19558				$scope.model.list.splice($scope.model.list.indexOf(itemId), 1);
19559				self.ngModelCtrl.$setViewValue($scope.model.list);
19560				loadOptions(false);
19561			}
19562
19563			function setModels(ngModel) {
19564				if ($scope.multiple) {
19565					if ($utils.isArray(ngModel)) {
19566						$scope.model.list = ngModel;
19567					} else if (ngModel) {
19568						$scope.model.list = ngModel.split(',');
19569					} else {
19570						$scope.model.list = [];
19571					}
19572				} else {
19573					$scope.model.id = ngModel;
19574				}
19575			}
19576
19577			function getSearchQuery() {
19578				var params = [];
19579				var search = $scope.model.search;
19580				if (!$utils.isBlank(search)) {
19581					params.push('_q=' + $utils.removeDiacritics(search.toLowerCase()));
19582				}
19583				if (self.query) {
19584					var query = $entity.parseQuery(self.query);
19585					params.push(query);
19586				}
19587				return params.join('&');
19588			}
19589
19590			function loadOptions(add) {
19591				if ($utils.isBlank(self.entity) || (typeof self.entity != 'string')) {
19592					return;
19593				}
19594				if (self.entity === 'entity' && $instance.getId() != null && (!$scope.omitInstances || !$user.isModeler())) {
19595					// load instance entities
19596					loadAllEntities();
19597					return;
19598				}
19599
19600				$timeout(function() {
19601					if (!scrollBind) {
19602						scrollBind = true;
19603						var uiSelectChoices = $element[0].querySelector('.select-geo-choices');
19604						if (uiSelectChoices) {
19605							scrollBind = true;
19606							angular.element(uiSelectChoices).bind('scroll', function() {
19607								onScroll(uiSelectChoices);
19608							});
19609						}
19610					}
19611				});
19612
19613				var searchQuery = getSearchQuery();
19614				var prevSearch = getOptionsBySearch();
19615				if (!add && prevSearch.loaded) {
19616					$scope.options = prepareOptions(prevSearch);
19617					return;
19618				}
19619
19620				var index = prevSearch.options.length;
19621				$constants.getDataListMin(self.entity, searchQuery, index, LIMIT, $scope.omitInstances).then(function(list) {
19622					var newOptions = list.map(function(item) {
19623						return item._id ? item._id : item.id;
19624					});
19625
19626					prevSearch.loaded = true;
19627					prevSearch.loadMore = newOptions.length === LIMIT;
19628					if (add) {
19629						prevSearch.options = prevSearch.options.concat(newOptions);
19630					} else {
19631						prevSearch.options = newOptions;
19632					}
19633
19634					$scope.options = prepareOptions(prevSearch);
19635				});
19636			}
19637
19638			function loadAllEntities() {
19639				var search = $scope.model.search;
19640				if (!!search) {
19641					search = search.toLowerCase();
19642				}
19643				
19644				var list = $utils.clone($instance.getEntityList());
19645				list.unshift({id: 'entity', name: $angular.get("$translate").instant("msg.entity")},
19646					{id: 'instance', name: $angular.get("$translate").instant("msg.instance")});
19647
19648				$scope.options = list.map(function(item) {
19649					return {
19650						id: item._id ? item._id : item.id,
19651						name: $entity.displayColumn(item)
19652					};
19653				}).filter(function(item) {
19654					return !search || (item.name && item.name.toLowerCase().contains(search));
19655				}).sort(function(itemA, itemB) {
19656					if (itemA.name < itemB.name) {
19657						return -1;
19658					}
19659					if (itemA.name > itemB.name) {
19660						return 1;
19661					}
19662					return 0;
19663				}).map(function(item) {
19664					return item.id;
19665				});
19666			}
19667
19668			function prepareOptions(newSearch) {
19669				var options = [];
19670				if ($utils.isTrue(self.emptyOption)) {
19671					options.push($constants.blank);
19672				}
19673				newSearch.options.forEach(function(option) {
19674					options.push(option);
19675				});
19676				if (self.ngModel && !$scope.multiple && !options.includes(self.ngModel)) {
19677					options.push(self.ngModel);
19678				}
19679				if ($scope.multiple && $scope.model.list) {
19680					return options.filter(function(option) {
19681						return !$scope.model.list.includes(option);
19682					});
19683				}
19684				return options;
19685			}
19686
19687			function onScroll(uiSelectChoices) {
19688				var remainingHeight = uiSelectChoices.offsetHeight - uiSelectChoices.scrollHeight;
19689				var scrollTop = uiSelectChoices.scrollTop;
19690				var percent = Math.abs((scrollTop / remainingHeight) * 100);
19691				if (percent > 80) {
19692					var prevSearch = getOptionsBySearch();
19693					if (prevSearch.loaded && prevSearch.loadMore) {
19694						prevSearch.loadMore = false;
19695						loadOptions(true);
19696					}
19697				}
19698			}
19699
19700			function getOptionsBySearch() {
19701				if (!self.entity) {
19702					return {
19703						options: [],
19704						loaded: true,
19705						loadMore: false
19706					};
19707				}
19708				var entityOptions = $scope.optionsBySearch[self.entity];
19709				if (!entityOptions) {
19710					entityOptions = {};
19711					$scope.optionsBySearch[self.entity] = entityOptions;
19712				}
19713				var searchQuery = getSearchQuery();
19714				var options = entityOptions[searchQuery];
19715				if (!options) {
19716					options = {
19717						options: [],
19718						loaded: false,
19719						loadMore: false
19720					};
19721					entityOptions[searchQuery] = options;
19722				}
19723				return options;
19724			}
19725		}
19726	};
19727})();
19728
19729//// File calendar.component.js ////
19730(function() {
19731
19732	$angular.components['calendar'] = {
19733		templateUrl: 'calendar.component.html',
19734		bindings: {
19735			config: '<'
19736		},
19737		controller: function($scope, $timeout, $element) {
19738			var self = $angular.prepareComponent(this, $scope, $timeout);
19739
19740			self.$onChanges = function() {
19741				$scope.refresh();
19742			};
19743
19744			$scope.$on('reloadCalendarComponent', reloadEvents);
19745
19746			$scope.refresh = function() {
19747				$scope.config = self.config;
19748				$scope.calendarLanguage = $locale.language;
19749				$scope.events = {};
19750				$scope.items = {};
19751				if ($scope.config.lazyLoadingEvents) {
19752					$scope.rangesLoaded = [];
19753				}
19754
19755				setTimeout(loadCalendar, 200);
19756			};
19757
19758			function loadCalendar() {
19759				if ($scope.config.lazyLoadingEvents) {
19760					createCalendar(function(info) {
19761						var from = info.start.getTime();
19762						var to = info.end.getTime();
19763						return getLazyEvents(from, to);
19764					});
19765				} else if ($scope.config.getItems) {
19766					Promise.all($scope.config.getItems().map(convertToEvent)).then(createCalendar);
19767				} else {
19768					createCalendar();
19769				}
19770
19771//				$angular.rootScope.$watch('language', function(value) {
19772//					if ($scope.calendar == null) {
19773//						return;
19774//					}
19775//					if (value !== $scope.calendarLanguage) {
19776//						$scope.calendarLanguage = value;
19777//						$scope.calendar.setOption('locale', value);
19778//						reloadEvents();
19779//					}
19780//				});
19781			}
19782
19783			function createCalendar(eventsProvider) {
19784			
19785				var height = "auto";
19786				var iv = localStorage.getItem("fcDefaultView") || 'dayGridMonth';
19787				var id = localStorage.getItem("fcDefaultDate") || new Date();
19788			
19789				var timeFormat = {
19790					omitZeroMinute: false,
19791					hour: '2-digit',
19792					minute: '2-digit',
19793					meridiem: false,
19794					hour12: false
19795				};
19796				var calendarParams = {
19797					headerToolbar: {
19798						left: 'prev,next today',
19799						center: 'title',
19800						right: 'dayGridMonth,customWeek,customDay,listMonth'
19801					},
19802					views: {
19803					    customWeek: {
19804					        type: 'dayGridWeek',
19805					        buttonText: 'week'
19806					    },
19807					    customDay: {
19808					        type: 'listDay',
19809					        buttonText: 'day'
19810					    }
19811					},
19812					initialView: iv,
19813				    initialDate: id,
19814				    datesSet: function (dateInfo) {
19815				        localStorage.setItem("fcDefaultView", dateInfo.view.type);
19816				        localStorage.setItem("fcDefaultDate", dateInfo.startStr);
19817				    },
19818					showNonCurrentDates: $scope.config.hideNonCurrentDates,
19819					locale: $scope.calendarLanguage,
19820					events: eventsProvider,
19821					eventClick: function(info) {
19822						if ($scope.config.onClickEvent) {
19823							var item = $scope.items[info.event.id];
19824							var event = $scope.events[info.event.id];
19825							$scope.config.onClickEvent(event, item);
19826						}
19827					},
19828					eventOrder: "start",
19829					eventDidMount: $scope.config.eventDidMount,
19830					eventMouseEnter: $scope.config.eventMouseEnter,
19831					eventMouseLeave: $scope.config.eventMouseLeave,
19832					slotLabelFormat: timeFormat,
19833					eventTimeFormat: timeFormat,
19834					weekends: true, navLinks: true, weekNumbers: false, height: height, firstDay:1
19835				};
19836				$scope.calendar = new FullCalendar.Calendar($element[0], calendarParams);
19837				$scope.calendar.render();
19838			}
19839
19840			function reloadEvents() {
19841				if ($scope.calendar == null) {
19842					return;
19843				}
19844				$scope.events = {};
19845				$scope.items = {};
19846				if ($scope.config.lazyLoadingEvents) {
19847					$scope.rangesLoaded = [];
19848					$scope.calendar.refetchEvents();
19849				} else {
19850					Promise.all($scope.config.getItems().map(convertToEvent)).then(function(events) {
19851						$scope.calendar.getEventSources()[0].remove();
19852						$scope.calendar.addEventSource(events);
19853					});
19854				}
19855			}
19856
19857			function getLazyEvents(from, to) {
19858				var cache = !$scope.config.noCache;
19859				var range = cache ? evaluateIfNeedsToLoadMoreItems(from, to) : true;
19860				if (range && $scope.config.buildUrl) {
19861					return $rest.get({
19862						url: $scope.config.buildUrl(from, to),
19863						loading: 'generic'
19864					}).then(function(o) {
19865						if (cache) {
19866							addRangeAsLoaded(range);
19867						}
19868						var items = $scope.config.getItemsFromResponse ? $scope.config.getItemsFromResponse($scope.calendar, o, from, to) : o;
19869						if (!Array.isArray(items)) {
19870							items = [items];
19871						}
19872						return Promise.all(items.map(convertToEvent));
19873					});
19874				}
19875				return Promise.resolve(Object.values($scope.events));
19876			}
19877
19878			function convertToEvent(item) {
19879				var event = $scope.config.convertItemToEvent ? $scope.config.convertItemToEvent(item) : item;
19880				if (typeof event.then === 'function') {
19881					return event.then(function(ev) {
19882						return prepareEvent(ev, item);
19883					});
19884				}
19885				return Promise.resolve(prepareEvent(event, item));
19886			}
19887
19888			function prepareEvent(event, item) {
19889				if (!event.id) {
19890					event.id = $utils.getRandom() + new Date().getTime();
19891				}
19892				$scope.items[event.id] = item;
19893				$scope.events[event.id] = event;
19894				return event;
19895			}
19896
19897			function evaluateIfNeedsToLoadMoreItems(from, to) {
19898				var range = {from: from, to: to};
19899				$scope.rangesLoaded.forEach(function(rangeIter) {
19900					if (range) {
19901						if (rangeIter.from <= range.from && rangeIter.to > range.from && rangeIter.to < range.to) {
19902							range.from = rangeIter.to;
19903						} else if (rangeIter.from > range.from && rangeIter.from < range.to && rangeIter.to >= range.to) {
19904							range.to = rangeIter.from;
19905						} else if (rangeIter.from <= range.from && rangeIter.to >= range.to) {
19906							range = null;
19907						}
19908					}
19909				});
19910				return range;
19911			}
19912
19913			function addRangeAsLoaded(range) {
19914				var newRanges = [];
19915				var added = null;
19916				$scope.rangesLoaded.forEach(function(rangeIter) {
19917					if (added) {
19918						if (added.to < rangeIter.from) {
19919							newRanges.push(rangeIter);
19920						} else if (added.to < rangeIter.to) {
19921							added.to = rangeIter.to;
19922						}
19923					} else if (range.from < rangeIter.from) {
19924						if (range.to < rangeIter.from) {
19925							added = {from: range.from, to: range.to};
19926							newRanges.push(added, rangeIter);
19927						} else if (range.to < rangeIter.to) {
19928							added = {from: range.from, to: rangeIter.to};
19929							newRanges.push(added);
19930						} else {
19931							added = {from: range.from, to: range.to};
19932							newRanges.push(added);
19933						}
19934					} else if (range.from <= rangeIter.to) {
19935						if (range.to <= rangeIter.to) {
19936							added = rangeIter;
19937						} else {
19938							added = {from: rangeIter.from, to: range.to};
19939						}
19940						newRanges.push(added);
19941					} else {
19942						newRanges.push(rangeIter);
19943					}
19944				});
19945				if (!added) {
19946					newRanges.push(range);
19947				}
19948				$scope.rangesLoaded = newRanges;
19949			}
19950		}
19951	};
19952})();
19953
19954//// File fidesur-reg.js ////
19955(function() {
19956
19957	$angular.controllers['fidesur-reg-component'] = function($scope, $element) {
19958		
19959		$angular.contentScope.submitFidesurReg = function() {
19960			$rest.put({
19961				url: contextPath + '/api/geo/' + $angular.contentScope.entity + '/' + $angular.contentScope.entityId + '/updateCustom.json',
19962				form: '#fidesurRegForm',
19963				loading: 'generic',
19964				successMessage: $angular.get('$translate').instant('msg.updated')
19965			}).then(function(data, url) {
19966				if ($angular.contentScope.postSubmit) {
19967		    		$angular.contentScope.postSubmit();
19968				}
19969			});
19970		};
19971		
19972		function verifyData() {
19973			var o = $angular.contentScope.model.fidesur_reg;
19974			if (o == null) {
19975				o = {};
19976				$angular.contentScope.model.fidesur_reg = o;
19977				var props = $angular.contentScope.model.geo_properties;
19978				if (props) {
19979					for (var k in props) {
19980						var prop = props[k];
19981						if (prop.name == 'Website') {
19982							o.empr_site = prop.value;
19983						} else if (prop.name == 'Phone') {
19984							o.empr_telf_fijo = prop.value;
19985						}
19986					}
19987				}
19988			}
19989			
19990			/* add one item for list */
19991			var btns = $('#fidesurRegForm .btn');
19992			btns.each(function() {
19993				var btn = $(this);
19994				var click = btn.attr('ng-click');
19995				if (click && click.startsWith('addList')) {
19996					setTimeout(function() {
19997						btn.click();
19998					}, 500);
19999				}
20000			});
20001			
20002		}
20003		verifyData();
20004		
20005		$angular.contentScope.getEntidades = function() {
20006			var $scope = $angular.contentScope;
20007			if ($scope.entidades && $scope.entidades.length > 0) {
20008				return $scope.entidades;
20009			}
20010			if ($scope.entidades) {
20011				return null;
20012			}
20013			$scope.entidades = [];
20014			$rest.post({
20015				url: contextPath + '/api/request.txt',
20016				loading: 'generic',
20017				data: { url : 'https://gaia.inegi.org.mx/wscatgeo/mgee/' }
20018			}).then(function(data) {
20019				$scope.entidades = angular.fromJson(data).datos;
20020			});
20021			return null;
20022		};
20023		
20024		$angular.contentScope.getMunicipios = function(entidad) {
20025			if (entidad == null) {
20026				return [];
20027			}
20028			var $scope = $angular.contentScope;
20029			if ($scope.municipios && $scope.municipios[entidad] && $scope.municipios[entidad].length > 0) {
20030				return $scope.municipios[entidad];
20031			}
20032			if ($scope.municipios && $scope.municipios[entidad]) {
20033				return null;
20034			}
20035			if ($scope.municipios == null) {
20036				$scope.municipios = {}; 
20037			}
20038			$scope.municipios[entidad] = [];
20039			$rest.post({
20040				url: contextPath + '/api/request.txt',
20041				loading: 'generic',
20042				data: { url : 'https://gaia.inegi.org.mx/wscatgeo/mgem/' + entidad }
20043			}).then(function(data) {
20044				$scope.municipios[entidad] = angular.fromJson(data).datos;
20045			});
20046			return null;
20047		};
20048		
20049		$angular.contentScope.getLocalidades = function(entidad, municipio) {
20050			if (entidad == null || municipio == null) {
20051				return [];
20052			}
20053			var $scope = $angular.contentScope;
20054			var key = entidad + '-' + municipio;
20055			if ($scope.localidades && $scope.localidades[key] && $scope.localidades[key].length > 0) {
20056				return $scope.localidades[key];
20057			}
20058			if ($scope.localidades && $scope.localidades[key]) {
20059				return null;
20060			}
20061			if ($scope.localidades == null) {
20062				$scope.localidades = {}; 
20063			}
20064			$scope.localidades[key] = [];
20065			$rest.post({
20066				url: contextPath + '/api/request.txt',
20067				loading: 'generic',
20068				data: { url : 'https://gaia.inegi.org.mx/wscatgeo/localidades/'+entidad+'/'+municipio }
20069			}).then(function(data) {
20070				$scope.localidades[key] = angular.fromJson(data).datos;
20071			});
20072			return null;
20073		};
20074		
20075		$angular.contentScope.selectEntidad = function(item) {
20076			var model = $angular.contentScope.model;
20077			if (model == null) {
20078				return;
20079			}
20080			if (item == null) {
20081				model.fidesur_reg.entidad_id = null; 
20082				model.fidesur_reg.entidad = null; 
20083				model.fidesur_reg.municipio_id = null; 
20084				model.fidesur_reg.municipio = null; 
20085				model.fidesur_reg.localidad_id = null;
20086				model.fidesur_reg.localidad = null;
20087				return;
20088			}
20089			model.fidesur_reg.entidad_id = item.cve_agee; 
20090			model.fidesur_reg.entidad = item.nom_agee; 
20091			model.fidesur_reg.municipio_id = null; 
20092			model.fidesur_reg.municipio = null; 
20093			model.fidesur_reg.localidad_id = null;
20094			model.fidesur_reg.localidad = null;
20095			$angular.contentScope.getMunicipios(model.fidesur_reg.entidad_id);
20096		};
20097		$angular.contentScope.selectMunicipio = function(item) {
20098			var model = $angular.contentScope.model;
20099			if (model == null) {
20100				return;
20101			}
20102			if (item == null) {
20103				model.fidesur_reg.municipio_id = null; 
20104				model.fidesur_reg.municipio = null; 
20105				model.fidesur_reg.localidad_id = null;
20106				model.fidesur_reg.localidad = null;
20107				return;
20108			}
20109			model.fidesur_reg.municipio_id = item.cve_agem; 
20110			model.fidesur_reg.municipio = item.nom_agem; 
20111			model.fidesur_reg.localidad_id = null;
20112			model.fidesur_reg.localidad = null;
20113			$angular.contentScope.getLocalidades(model.fidesur_reg.entidad_id, model.fidesur_reg.municipio_id);
20114		};
20115		$angular.contentScope.selectLocalidad = function(item) {
20116			var model = $angular.contentScope.model;
20117			if (model == null) {
20118				return;
20119			}
20120			if (item == null) {
20121				model.fidesur_reg.localidad_id = null;
20122				model.fidesur_reg.localidad = null;
20123				return;
20124			}
20125			model.fidesur_reg.localidad_id = item.cve_loc;
20126			model.fidesur_reg.localidad = item.nom_loc;
20127		};
20128		
20129		$angular.contentScope.getCodigoInegi = function() {
20130			/* codigo 00(clave estado)000(municipio)0000(localidad) */
20131			var o = $angular.contentScope.model.fidesur_reg; 
20132			if (o && o.entidad_id) {
20133				var cod = o.entidad_id;
20134				if (o.municipio_id) {
20135					cod += o.municipio_id;
20136					if (o.localidad_id) {
20137						cod += o.localidad_id;
20138					}
20139				}
20140				return cod;
20141			}
20142			return '';
20143		};
20144		
20145		$angular.contentScope.getTipos = function() {
20146			var o = $angular.contentScope.model.fidesur_reg;
20147			if (o && o.prod_categoria) {
20148				var cat = $angular.contentScope.categories[o.prod_categoria];
20149				if (cat && cat.tipos) {
20150					return cat.tipos;
20151				}
20152			}
20153			return {};
20154		};
20155		$angular.contentScope.getSubtipos = function() {
20156			var o = $angular.contentScope.model.fidesur_reg;
20157			if (o && o.prod_categoria) {
20158				var cat = $angular.contentScope.categories[o.prod_categoria];
20159				if (cat && cat.tipos) {
20160					var tipo = cat.tipos[o.prod_tipo];
20161					if (tipo && tipo.subtipos) {
20162						return tipo.subtipos;
20163					}
20164				}
20165			}
20166			return {};
20167		};
20168		$angular.contentScope.getCodigoProd = function() {
20169			/* codigo attractivo 000(categoria)A(tipo)00(subtipo)-1(consecutivo) */
20170			var o = $angular.contentScope.model.fidesur_reg; 
20171			if (o && o.prod_categoria) {
20172				var cod = o.prod_categoria;
20173				if (o.prod_tipo) {
20174					cod += '-' + o.prod_tipo;
20175					if (o.prod_subtipo) {
20176						cod += o.prod_subtipo;
20177					}
20178				}
20179				return cod;
20180			}
20181			return '';
20182		};
20183		
20184		$angular.contentScope.categories = {
20185			'SN' : { name: 'Sitios Naturales', tipos: {
20186					'M': { name: 'Montañas', subtipos: { '1': 'Altiplano', '2': 'Meseta', '3': 'Cerro', '4': 'Áreas nevadas', '5': 'Bosque de piedras', '6': 'Volcanes' } },
20187					'P': { name: 'Planicies', subtipos: { '1': 'Desiertos', '2': 'Llanuras', '3': 'Pampas', '4': 'Salinas' } },
20188					'V': { name: 'Valles', subtipos: { '1': 'Valles' } },
20189					'C': { name: 'Cañones y quebradas', subtipos: { '1': 'Quebradas', '2': 'Cañones' } },
20190					'LL': { name: 'Lagos, lagunas, pantanos, presas y esteros', subtipos: { '1': 'Lagos', '2': 'Laguna', '3': 'Pantano', '4': 'Estero', '5': 'Presa' } },
20191					'R': { name: 'Ríos y arroyos', subtipos: { '1': 'Río', '2': 'Arroyo' } },
20192					'CM': { name: 'Caída de agua y manantiales', subtipos: { '1': 'Caída de agua y manantiales', '2': 'Manantiales' } },
20193					'AT': { name: 'Aguas termales y minero-medicinales', subtipos: { '1': 'Aguas termales', '2': 'Aguas minero-medicinales' } },
20194					'L': { name: 'Litorales', subtipos: { '1': 'Playas', '2': 'Bahías', '3': 'Penínsulas', '4': 'Puntas', '5': 'Islas', '6': 'Caletas', '7': 'Manglares' } },
20195					'G': { name: 'Grutas, cavernas y cuetas', subtipos: { '1': 'Grutas, cavernas y cuetas', '2': 'Cuevas' } },
20196					'ANP': { name: 'Áreas Naturales Protegidas', subtipos: { '1': 'Reservas de la Biósfera', '2': 'Parques Nacionales', '3': 'Parques Nacionales', '4': 'Áreas de Protección de Recursos Naturales', '5': 'Áreas de Protección de Flora y Fauna', '6': 'Santuarios' } },
20197					'O': { name: 'Otros', subtipos: { '1': 'Bosques', '2': 'Selvas', '3': 'Miradores naturales', '4': 'Lugares paleontológicos' } }
20198			} },	
20199			'MHC' : { name: 'Manifestaciones Histórico Culturales', tipos: {
20200					'M': { name: 'Museos', subtipos: { '1': 'Arqueológico / Antropológico', '2': 'Arqueológico / Antropológico', '3': 'Historia natural', '4': 'Arte', '5': 'Historia', '6': 'Otros' } },
20201					'A': { name: 'Arquitectura y espacios urbanos', subtipos: { '1': 'Iglesia', '2': 'Convento', '3': 'Capilla', '4': 'Casa de valor arquitectónico', '5': 'Casa - Hacienda', '6': 'Palacio o castillo', '7': 'Teatros', '8': 'Biblioteca', '9': 'Molino', '10': 'Edificaciones civiles (fortalezas, escuelas, colegios, etc.)', '11': 'Universidades', '12': 'Otros', '13': 'Plazas', '14': 'Alamedas', '15': 'Paseos', '16': 'Boulevares', '17': 'Malecones', '18': 'Santuarios', '19': 'Miradores', '20': 'Parques y Jardines', '21': 'Glorietas', '22': 'Puerto o embarcadero', '23': 'Obras de ingeniería (puentes, estación ferroviaria, etc.)' } },
20202					'LH': { name: 'Lugares Históricos', subtipos: { '1': 'Casco urbano histórico', '2': 'Edificaciones civiles', '3': 'Edificiacione religiosas', '4': 'Barrios', '5': 'Puertos o embarcaderos', '6': 'Campo santo', '7': 'Campo de batalla', '8': 'Otros', '9': 'Obras de ingeniería (puentes, c
20202anales, etc.)' } },
20203					'R': { name: 'Ruinas y Sitios Arqueológicos', subtipos: { '1': 'Edificaciones', '2': 'Pinturas rupestres', '3': 'Petroglifos', '4': 'Esculturas', '5': 'Geoglifos', '6': 'Sitios funerarios', '7': 'Otros' } },
20204					'P': { name: 'Pueblos', subtipos: { '1': 'Pueblos históricos', '2': 'Pueblos tradicionales' } }
20205			} },	
20206			'F' : { name: 'Folklore', tipos: {
20207					'MR': { name: 'Manifestaciones religiosas y creencias populares', subtipos: { '1': 'Literatura', '2': 'Leyendas', '3': 'Mitos', '4': 'Ceremonias y costumbres religiosas' } },
20208					'FM': { name: 'Ferias y mercados', subtipos: { '1': 'Mercados artesanales', '2': 'Tianguis', '3': 'Ferias artesanales' } },
20209					'MD': { name: 'Música y Danzas', subtipos: { '1': 'Música y Danza' } },
20210					'A': { name: 'Artesanías y artes', subtipos: { '1': 'Alfarería', '2': 'Cera', '3': 'Cerámica', '4': 'Cueros y pieles', '5': 'Madera / carpintería', '6': 'Instrumentos musicales', '7': 'Metales', '8': 'Máscaras', '9': 'Objetos rituales', '10': 'Papel', '11': 'Piedras', '12': 'Pinturas', '13': 'Tejidos/indumentaria' } },
20211					'G': { name: 'Gastronomía', subtipos: { '1': 'Platillos típicos', '2': 'Bebidas típicas', '3': 'Dulces típicos' } }
20212			} },	
20213			'RT' : { name: 'Realizaciones Técnicas, Científicas o Artísticas Contemporáneas', tipos: {
20214					'EM': { name: 'Explotaciones mineras', subtipos: { '1': 'Explotaciones mineras' } },
20215					'EA': { name: 'Explotaciones agropecuarias o pesqueras', subtipos: { '1': 'Agricultura', '2': 'Ganadería / Granjas', '3': 'Piscigranjas' } },
20216					'EI': { name: 'Explotaciones industriales', subtipos: { '1': 'Fábricas', '2': 'Tra
20216piches', '3': 'Bodegas', '4': 'Destiladoras y vitivinícolas' } },
20217					'C': { name: 'Centros Científicos y Técnicos', subtipos: { '1': 'Zoológicos', '2': 'Jardines Botánicos', '3': 'Planetarios', '4': 'Acuarios', '5': 'Viveros', '6': 'Túneles', '7': 'Puentes', '8': 'Hidroeléctricas', '9': 'Represas' } },
20218					'P': { name: 'Planicies', subtipos: { '1': 'Desiertos', '2': 'Llanuras', '3': 'Pampas', '4': 'Salinas' } },
20219					'O': { name: 'Otros' }
20220			} },
20221			'AP' : { name: 'Acontecimientos Programados' }
20222		};
20223		
20224	};
20225})();
20226
20227//// File columns.component.js ////
20228(function() {
20229	
20230	$angular.components['columns'] = {
20231		templateUrl: 'columns.component.html',
20232		bindings: { meta: '=', model: '<', columns: '=?', opts: '=?', ext: '=?', ientity: '=?' },
20233		controller: function($scope, $timeout) {
20234			var self = $angular.prepareComponent(this, $scope, $timeout);
20235
20236			$scope.refresh = function() {
20237				if (self.columns) {
20238
20239					//console.log('#' + $scope.$id + ' init columns:' + self.columns.length + ' -> ' + getColumnNames());
20240					$utils.initColumns($scope, self.meta, self.columns, self.model, self.opts, self.ext, self.ientity);
20241
20242					self.$onChanges = function(val) {
20243						if (val.model) {
20244							//console.log('model changed', val.model);
20245							$scope.refresh();
20246						}
20247					};
20248
20249				} else {
20250					$angular.log('warn: init columns with no columns')
20251				}
20252			};
20253
20254//			function getColumnNames() {
20255//				var list = [];
20256//				for (var k in self.columns) {
20257//					if (list.length == 5) {
20258//						list.push("...");
20259//						break;
20260//					}
20261//					var col = self.columns[k];
20262//					list.push(col.name);
20263//				}
20264//				return $utils.join(list, ", ");
20265//			}
20266
20267			$scope.removeColumnItem = function(data, column, index, item) {
20268
20269				if ($utils.isBlank(item.id)) {
20270					// remove from list
20271					if (data[column.name] && $utils.isArray(data[column.name])) {
20272						data[column.name].splice(index, 1);
20273					}
20274					return;
20275				}
20276
20277				// remove from server
20278				//console.log(entity + " -> " + id)
20279				$rest.remove({
20280					url: contextPath + "/api/geo/" + item.entity + "/" + item.id + ".json?force=true",
20281					loading: "generic",
20282					confirmTitle: $angular.get("$translate").instant("msg.remove"),
20283					successMessage: $angular.get("$translate").instant('msg.removed')
20284				}).then(function() {
20285					$angular.contentScope.refresh();
20286				});
20287			};
20288
20289		}
20290	}
20291	
20292})();
20293
20294//// File spreadsheet-filters.component.js ////
20295(function() {
20296
20297	$angular.controllers['spreadsheet-filters-component'] = function($scope) {
20298
20299		$scope.$on('initSpreadsheetFiltersComponent', function(event, config) {
20300			$scope.spreadsheet = config.model;
20301			$scope.model = prepareModel();
20302		});
20303
20304		$scope.$on('refreshSpreadsheetFiltersComponent', function(event, data) {
20305			$scope.spreadsheet = data.model;
20306			$scope.model = prepareModel();
20307		});
20308
20309		$scope.$watch('model.custom', function(val) {
20310			if (!$scope.spreadsheet.filters) {
20311				$scope.spreadsheet.filters = {};
20312			}
20313			$scope.spreadsheet.filters.custom = val;
20314		});
20315
20316		$scope.$watch('model.query', function(val) {
20317			if (!$scope.spreadsheet.filters) {
20318				$scope.spreadsheet.filters = {};
20319			}
20320			$scope.spreadsheet.filters.query = val;
20321		});
20322
20323		$scope.buildQuery = function() {
20324			var entity = $scope.spreadsheet.entities && $scope.spreadsheet.entities.length > 0 ? $scope.spreadsheet.entities[0] : null;
20325			$angular.openModalLocal('panel-query-builder', {
20326				size: 'xl',
20327				entity: entity,
20328				query: $scope.model.query
20329			}).then(function(result) {
20330				$scope.model.query = result;
20331			});
20332		};
20333
20334		function prepareModel() {
20335			var model = {
20336				custom: false,
20337				columns: []
20338			};
20339			var filters = $scope.spreadsheet.filters;
20340			if (filters) {
20341				model.query = filters.query;
20342				model.custom = $utils.isTrue(filters.custom);
20343				if (filters.columns) {
20344					model.columns = angular.fromJson(filters.columns);
20345				}
20346			}
20347			return model;
20348		}
20349
20350		$scope.onChange = function(columns) {
20351			if (!$scope.spreadsheet.filters) {
20352				$scope.spreadsheet.filters = {};
20353			}
20354			$scope.spreadsheet.filters.columns = angular.toJson(columns);
20355		};
20356	}
20357})();
20358
20359//// File fidesur-eval.js ////
20360(function() {
20361
20362	$angular.controllers['fidesur-eval-component'] = function($scope, $element) {
20363		
20364		function verifyData() {
20365			var o = $angular.contentScope.model.fidesur_eval;
20366			if (o == null) {
20367				o = {};
20368				$angular.contentScope.model.fidesur_eval = o;
20369			}
20370		}
20371		verifyData();
20372		
20373		$angular.contentScope.submitFidesurEval = function() {
20374			$rest.put({
20375				url: contextPath + '/api/geo/' + $angular.contentScope.entity + '/' + $angular.contentScope.entityId + '/updateCustom.json',
20376				form: '#fidesurEvalForm',
20377				loading: 'generic',
20378				successMessage: $angular.get('$translate').instant('msg.updated')
20379			}).then(function(data, url) {
20380				if ($angular.contentScope.postSubmit) {
20381		    		$angular.contentScope.postSubmit();
20382				}
20383			});
20384		};
20385	}
20386	
20387})();
20388//// File input-check.js ////
20389(function() {
20390	
20391	$angular.components['inputCheck'] = {
20392		//transclude: true,
20393		templateUrl: "input-check.html",
20394		bindings: {
20395			key: '@', model: '=', label: '@', required: '@', changed: '&'
20396		},
20397		controller: function($scope, $timeout) {
20398			$scope.rw = $utils.getRandom();
20399			var self = this;
20400			$timeout(function() {
20401				self.model = $utils.isTrue(self.model);
20402//				console.log("key: " + self.key);
20403//				console.log("model", self.model);
20404//				console.log("msg: " + self.msg);
20405			});
20406		}
20407	}
20408	
20409})();	
20410//// File cart-content.component.js ////
20411(function() {
20412
20413	$angular.components['cartContent'] = {
20414		templateUrl: 'cart-content.component.html',
20415		bindings: {
20416			cart: '<',
20417			readonly: '<',
20418			showFormEntities: '<',
20419			onChange: '&'
20420		},
20421		controller: function($scope, $timeout) {
20422			var self = $angular.prepareComponent(this, $scope, $timeout);
20423
20424			self.$onChanges = function() {
20425				$scope.refresh();
20426			};
20427
20428			$scope.refresh = function() {
20429
20430				$scope.readonly = self.readonly;
20431				$scope.onChange = self.onChange;
20432				$scope.cart = self.cart;
20433
20434				$scope.formConfigs = {};
20435
20436				if (self.showFormEntities) {
20437					prepareFormConfigs();
20438				}
20439			};
20440
20441			$scope.remove = function(item) {
20442				$cart.removeItem(item);
20443				emitOnChange();
20444			};
20445
20446			$scope.addQuantity = function(item) {
20447				item.quantity++;
20448				$scope.modifiedQuantity(item);
20449			};
20450
20451			$scope.removeQuantity = function(item) {
20452				if (item.quantity === 1) {
20453					return;
20454				}
20455				item.quantity--;
20456				$scope.modifiedQuantity(item);
20457			};
20458
20459			$scope.modifiedQuantity = function(item) {
20460				if (isValidQuantity(item)) {
20461					$cart.addItem(item.product.entity, item.product.entity_id, item);
20462					emitOnChange();
20463				}
20464			};
20465
20466			function emitOnChange() {
20467				if ($scope.onChange) {
20468					var ready = Object.keys($scope.formConfigs).every(function(key) {
20469						var config = $scope.formConfigs[key];
20470						return config == null || config.ready;
20471					});
20472					$scope.onChange({
20473						isReady: ready && !hasInvalidQuantities()
20474					});
20475				}
20476			}
20477
20478			function hasInvalidQuantities() {
20479				if (!$scope.readonly) {
20480					var items = $scope.cart.items;
20481					if (items) {
20482						for (var iter = 0; iter < items.length; iter++) {
20483							var item = items[iter];
20484							if (!isValidQuantity(item) || (item.stock != null && item.stock < item.quantity)) {
20485								return true;
20486							}
20487						}
20488					}
20489				}
20490				return false;
20491			}
20492
20493			function isValidQuantity(item) {
20494				var quantity = item.quantity;
20495				return quantity && !isNaN(quantity) && quantity > 0 && quantity < 1000000;
20496			}
20497
20498			function prepareFormConfigs() {
20499				var items = $scope.cart ? $scope.cart.items : null;
20500				if (!items) {
20501					return;
20502				}
20503
20504				items.forEach(function(cartItem) {
20505					var key = cartItem.product.entity + ';' + cartItem.product.entity_id;
20506					var formEntity = cartItem.extra_data_entity;
20507					if (formEntity && $entity.isGeoForm(formEntity) && $entity.isPermitted(formEntity, 'create')) {
20508						var config = {
20509							entity: $entity.getEntity(formEntity),
20510							ready: true
20511						};
20512						config.onChange = function(formModel, isReady) {
20513							config.ready = isReady;
20514							cartItem.extra_data = formModel;
20515							emitOnChange();
20516						};
20517						$scope.formConfigs[key] = config;
20518					}
20519				});
20520				emitOnChange();
20521			}
20522		}
20523	};
20524})();
20525
20526//// File price-breakdown.component.js ////
20527(function() {
20528
20529	$angular.components['priceBreakdown'] = {
20530		templateUrl: 'price-breakdown.component.html',
20531		bindings: {cart: '<', hideTip: '<', hideShippingCosts: '<'},
20532		controller: function($scope, $timeout) {
20533			var self = $angular.prepareComponent(this, $scope, $timeout);
20534
20535			self.$onChanges = function() {
20536				$scope.refresh();
20537			};
20538
20539			$scope.refresh = function() {
20540				$scope.hideTip = self.hideTip;
20541				$scope.hideShippingCosts = self.hideShippingCosts;
20542				prepareData(self.cart);
20543			};
20544
20545			function prepareData(cart) {
20546				if (cart) {
20547					var cartSubtotal = 0;
20548					var taxes = {};
20549					var dataItems = [];
20550					for (var i in cart.items) {
20551						var item = cart.items[i];
20552						var price = 0;
20553						var discount = 0;
20554						if (!$utils.isTrue(item.free)) {
20555							if (item.price > 0) {
20556								price = Number(item.price);
20557							}
20558							if (item.discount > 0) {
20559								discount = Number(item.discount);
20560							}
20561						}
20562						var amountWithoutTaxes = price - discount;
20563						if (amountWithoutTaxes < 0) {
20564							amountWithoutTaxes = 0;
20565						}
20566						var taxAmount = 0;
20567						if (amountWithoutTaxes > 0 && item.tax && !isNaN(item.tax)) {
20568							var tax = taxes[item.tax];
20569							if (!tax) {
20570								tax = 0;
20571							}
20572							taxAmount = (Math.round((amountWithoutTaxes * Number(item.tax)) + Number.EPSILON) / 100);
20573							taxes[item.tax] = tax + (taxAmount * item.quantity);
20574						}
20575						cartSubtotal += item.amount * item.quantity;
20576						dataItems.push({
20577							name: item.name,
20578							quantity: item.quantity,
20579							unitAmount: amountWithoutTaxes,
20580							totalAmount: amountWithoutTaxes * item.quantity
20581						});
20582					}
20583
20584					var cartTotal = cartSubtotal;
20585					if (!$scope.hideTip && cart.tip && !isNaN(cart.tip)) {
20586						cartTotal += cart.tip;
20587					}
20588
20589					if (!$scope.hideShippingCosts && cart.delivery_method && cart.delivery_method.price && !isNaN(cart.delivery_method.price)) {
20590						cartTotal += cart.delivery_method.price;
20591					}
20592
20593					cartTotal = Math.round((cartTotal + Number.EPSILON) * 100) / 100;
20594					cartSubtotal = Math.round((cartSubtotal + Number.EPSILON) * 100) / 100;
20595
20596					$scope.data = {
20597						items: dataItems,
20598						taxes: Object.keys(taxes).length > 0 ? taxes : null,
20599						totalAmount: cartTotal,
20600						subtotalAmount: cartSubtotal,
20601						tip: cart.tip,
20602						shippingCosts: cart.delivery_method ? cart.delivery_method.price : null
20603					};
20604				} else {
20605					$scope.data = null;
20606				}
20607			}
20608		}
20609	};
20610})();
20611
20612//// File login.component.js ////
20613(function() {
20614
20615	$angular.components['login'] = {
20616		templateUrl: 'login.component.html',
20617		bindings: {
20618			hideTitle: '<',
20619			onLogin: '&',
20620			onCancel: '&'
20621		},
20622		controller: function($scope, $timeout, $attrs) {
20623			var self = $angular.prepareComponent(this, $scope, $timeout);
20624
20625			self.$onChanges = function() {
20626				$scope.refresh();
20627			};
20628
20629			$scope.refresh = function() {
20630
20631				$scope.user = null;
20632				$scope.model = {};
20633				$scope.showTitle = !$utils.isTrue(self.hideTitle);
20634
20635				$scope.showSignUp = $constants.isWeb;
20636				if ($instance.current && $instance.current.geo_login) {
20637					var login = $instance.current.geo_login;
20638					if ($utils.isTrue(login.disabled_register)) {
20639						$scope.showSignUp = false;
20640					}
20641					if ($utils.isTrue(login.disabled)) {
20642						self.onCancel();
20643						$utils.showAccessDenied();
20644					}
20645				}
20646			};
20647
20648			function hideAll() {
20649				$scope.showBack = false;
20650				$("#login").hide();
20651				$("#register").hide();
20652				$("#update").hide();
20653				$("#restore").hide();
20654				$("#restoring").hide();
20655				$("#verify").hide();
20656			}
20657
20658			$scope.back = function() {
20659				hideAll();
20660				$("#login").fadeIn();
20661
20662				if ($scope.user) {
20663					$scope.signOut();
20664				}
20665			};
20666			$scope.showRegister = function() {
20667				hideAll();
20668				$scope.model.user_id = "";
20669				$scope.model.name = "";
20670				$("#register").fadeIn();
20671				$scope.showBack = true;
20672			};
20673			$scope.showUpdate = function() {
20674				hideAll();
20675				$("#update").fadeIn();
20676				$scope.showBack = true;
20677			};
20678			$scope.showRestore = function() {
20679				hideAll();
20680				$("#restore").fadeIn();
20681				$scope.showBack = true;
20682			};
20683			$scope.showRestoring = function() {
20684				hideAll();
20685				$("#restoring").fadeIn();
20686				$scope.showBack = true;
20687			};
20688			$scope.showVerify = function() {
20689				hideAll();
20690				$("#verify").fadeIn();
20691				$scope.showBack = true;
20692				$scope.sendEmailVerify();
20693			};
20694
20695			$scope.signOut = function(callback) {
20696				$scope.user = null;
20697				firebase.auth().signOut().then(function() {
20698					if (callback) {
20699						callback();
20700					}
20701				}, function() {
20702					if (callback) {
20703						callback();
20704					}
20705				});
20706			};
20707
20708			$scope.login = function() {
20709				$utils.closeFloating();
20710
20711				var email = $scope.model.email;
20712				var password = $scope.model.password;
20713				if ($utils.isBlank(email) || $utils.isBlank(password)) {
20714					return;
20715				}
20716
20717				$utils.showLoading();
20718				$scope.signOut(function() {
20719					firebase.auth().signInWithEmailAndPassword(email, password).then(function(o) {
20720						$utils.hideLoading();
20721
20722						var user = o.user;
20723						$scope.user = user;
20724						if (!user.emailVerified) {
20725							$scope.showVerify();
20726							return;
20727						}
20728
20729						// login ok
20730						$scope.model.token = user.refreshToken;
20731						$scope.model.password = null;
20732						$angular.apply($scope);
20733						$scope.loginFinal();
20734
20735					}).catch(function() {
20736						$utils.hideLoading();
20737
20738						$scope.model.token = "";
20739						$angular.apply($scope);
20740						$scope.loginFinal();
20741					});
20742				});
20743			};
20744
20745			$scope.loginWithGoogle = function() {
20746				$utils.closeFloating();
20747				$utils.showLoading();
20748				
20749				var provider = new firebase.auth.GoogleAuthProvider();
20750				firebase.auth().signInWithPopup(provider).then(function(result) {
20751					$utils.hideLoading();
20752
20753					var user = result.user;
20754					$scope.user = user;
20755					
20756					// login ok
20757					$scope.model.email = user.email;
20758					$scope.model.token = user.refreshToken || (user.stsTokenManager && user.stsTokenManager.refreshToken);
20759					$scope.model.password = null;
20760					$angular.apply($scope);
20761					$scope.loginFinal();
20762
20763				}).catch(function(error) {
20764					$utils.hideLoading();
20765					console.error("Google sign in error", error);
20766					resolveError(error, error.message);
20767				});
20768			};
20769
20770			$scope.loginFinal = function() {
20771				$rest.post({
20772					loading: "generic",
20773					url: contextPath + "/log-in",
20774					contentType: "urlencoded",
20775					form: "#loginForm",
20776					showError: false
20777				}).then(function() {
20778					$user.set($attrs.onLogin ? self.onLogin : null);
20779
20780				}).catch(function(e) {
20781					resolveError(e, "msg.login.error");
20782				});
20783			}
20784
20785			$scope.register = function() {
20786				$utils.closeFloating();
20787
20788				if (!$utils.isTrue($scope.model.licenses)) {
20789					return;
20790				}
20791
20792				$rest.post({
20793					loading: "generic",
20794					url: contextPath + "/www/user.json",
20795					contentType: "urlencoded",
20796					form: "#registerForm",
20797					showError: false,
20798					successMessage: $angular.get("$translate").instant("msg.created")
20799				}).then(function() {
20800					$scope.back();
20801				}).catch(function(e) {
20802					resolveError(e);
20803				});
20804			};
20805
20806			$scope.update = function() {
20807				$utils.closeFloating();
20808
20809				if (!$utils.isTrue($scope.model.licenses)) {
20810					return;
20811				}
20812
20813				$rest.post({
20814					loading: "generic",
20815					url: contextPath + "/www/user/a/update.json",
20816					contentType: "urlencoded",
20817					form: "#updateForm",
20818					showError: false,
20819					successMessage: $angular.get("$translate").instant("msg.updated")
20820				}).then(function() {
20821					$scope.back();
20822
20823				}).catch(function(e) {
20824					resolveError(e);
20825				});
20826			};
20827
20828			$scope.restore = function() {
20829				$utils.closeFloating();
20830
20831				if ($utils.isBlank($scope.model.email) || !$utils.isEmail($scope.model.email)) {
20832					console.log('invalid email')
20833					return;
20834				}
20835				
20836				$rest.post({
20837					loading: "generic",
20838					url: contextPath + "/www/user/a/restore.json",
20839					data: $scope.model,
20840					showError: false
20841				}).then(function() {
20842					$scope.showRestoring();
20843				}).catch(function() {
20844					$scope.showRestoring();
20845				});
20846			};
20847
20848			$scope.sendEmailVerify = function() {
20849				var email = $scope.model.email;
20850				var password = $scope.model.password;
20851				$utils.showLoading();
20852
20853				if ($scope.user && $scope.user.email === email) {
20854					sendEmail();
20855					return;
20856				}
20857
20858				firebase.auth().signInWithEmailAndPassword(email, password).then(function(o) {
20859					$scope.user = o.user;
20860					sendEmail();
20861
20862				}).catch(function() {
20863					$utils.hideLoading();
20864					$utils.floatingError($angular.get("$translate").instant("msg.error"));
20865					$scope.back();
20866				});
20867			};
20868
20869			function sendEmail() {
20870				$rest.post({
20871					loading: "generic",
20872					url: contextPath + "/www/user/a/verify.json",
20873					data: $scope.model,
20874					showError: false
20875				}).then(function() {
20876					$utils.hideLoading();
20877				}).catch(function() {
20878					$utils.hideLoading();
20879					$utils.floatingError($angular.get("$translate").instant("msg.error"));
20880					$scope.back();
20881				});
20882			}
20883
20884			$scope.verify = function() {
20885				$utils.closeFloating();
20886
20887				var email = $scope.model.email;
20888				var password = $scope.model.password;
20889				$utils.showLoading();
20890				firebase.auth().signInWithEmailAndPassword(email, password).then(function(o) {
20891					$utils.hideLoading();
20892
20893					var user = o.user;
20894					$scope.user = user;
20895					if (user.emailVerified) {
20896						$scope.back();
20897						$scope.login();
20898						return;
20899					}
20900					$utils.floatingError($angular.get("$translate").instant("msg.verify.error"));
20901
20902				}).catch(function(error) {
20903					$utils.hideLoading();
20904					console.log('error verifying: ' + error.message);
20905					$utils.floatingError($angular.get("$translate").instant("msg.error"));
20906					$scope.back();
20907				});
20908
20909			};
20910
20911			function resolveError(e, msg) {
20912				//console.log('error login', e.response)
20913
20914				if (e && e.response && e.response.status === 409) {
20915
20916					if (e.response.data && e.response.data.key === "UPDATE") {
20917						$scope.showUpdate();
20918						$scope.model.user_id = e.response.data.user_id;
20919						$scope.model.name = e.response.data.name;
20920						$scope.model.username = $scope.model.email;
20921						$scope.model.password_old = $scope.model.password;
20922						return true;
20923					}
20924
20925					if (e.response.data && e.response.data.key === "NOT_VERIFIED") {
20926						$scope.showVerify();
20927						return true;
20928					}
20929
20930					if (e.response.data && e.response.data.key === "RESTORE") {
20931						$scope.showRestore();
20932						return true;
20933					}
20934
20935					if (e.response.data && e.response.data.key === "APP_ERROR") {
20936						$utils.floatingError($angular.get("$translate").instant(e.response.data.messageKey));
20937						return true;
20938					}
20939
20940				}
20941
20942				if (msg) {
20943					$utils.floatingError($angular.get("$translate").instant(msg));
20944				} else if (e.msg) {
20945					$utils.floatingError(e.msg);
20946				} else {
20947					$utils.floatingError($angular.get("$translate").instant("msg.error"));
20948				}
20949
20950				return false;
20951			}
20952		}
20953	};
20954})();
20955
20956//// File entity-columns.component.js ////
20957(function() {
20958	
20959	$angular.components['entityColumns'] = {
20960		templateUrl: 'entity-columns.component.html',
20961		bindings: {
20962			model: '<', noEdit: '<', showInfo: '<', noJson: '<',
20963			labeled: '<', rowColPosition: '<', compareOperators: '<', autopathEnabled: '<', autopathRootEntity: '<',
20964			onChange: '&'
20965		},
20966		controller: function($scope, $timeout) {
20967			var self = $angular.prepareComponent(this, $scope, $timeout);
20968
20969			//console.log('init columns ' + $scope.$id + ' controller');
20970			$scope.refresh = function() {
20971				if (self.model) {
20972
20973					//console.log('#' + $scope.$id + ' init entity columns', self.model);
20974					$scope.entity = $angular.contentScope.entity;
20975					$scope.editImage = $angular.contentScope.editImage;
20976
20977					$scope.model = self.model;
20978					$scope.editEnabled = !$utils.isTrue(self.noEdit);
20979					$scope.jsonEditEnabled = !$utils.isTrue(self.noJson) && $user.isModeler();
20980					$scope.showInfo = $utils.isTrue(self.showInfo);
20981					$scope.labeled = $utils.isTrue(self.labeled);
20982					$scope.rowColPosition = $utils.isTrue(self.rowColPosition);
20983					$scope.compareOperators = $utils.isTrue(self.compareOperators);
20984					$scope.autopathEnabled = $utils.isTrue(self.autopathEnabled);
20985
20986					if ($scope.entity === 'entity' && $scope.model.columns) {
20987						// save last name/type to detect column renames etc
20988						$scope.model.columns.forEach(function(col) {
20989							col._name = col.name;
20990							col._type = col.type;
20991						});
20992					}
20993
20994					$scope.$watch('model', self.onChange, true);
20995
20996					self.$onChanges = function(val) {
20997						if (val.model) {
20998							console.log('entity columns model changed', val.model);
20999							$scope.refresh();
21000						}
21001					};
21002
21003				} else {
21004					$angular.log('warn: init entity columns with no model')
21005				}
21006			};
21007
21008			$scope.addColumn = function(o) {
21009				if (o.columns == null) {
21010					o.columns = [];
21011				}
21012				var column = {};
21013				if ($scope.entity === 'entity') {
21014					column.type = 'string';
21015				} else if ($scope.entity === 'form') {
21016					column.long = true;
21017				}
21018				o.columns.push(column);
21019			};
21020			$scope.removeColumn = function(o, index) {
21021				o.columns.splice(index, 1);
21022			};
21023
21024			$scope.editColumnsAsJson = function(obj) {
21025				if (!obj.columns) {
21026					obj.columns = [];
21027				}
21028				$angular.openModal('panel-edit-json', {
21029					json: obj.columns,
21030					size: 'lg',
21031					successFunction: function(json) {
21032						if (Array.isArray(json)) {
21033							obj.columns.length = 0;
21034							obj.columns.push.apply(obj.columns, json);
21035						}
21036					}
21037				});
21038			};
21039
21040			$scope.reloadFormulas = function(obj) {
21041				$rest.post({
21042					url: contextPath + "/api/geo/" + obj.id + "/a/reloadFormulas.json",
21043					loading: "generic",
21044					successMessage: $angular.get("$translate").instant('msg.done')
21045				}).then(function(o) {
21046				});
21047			};
21048
21049			$scope.showChecks = null;
21050			$scope.editChecks = function(key) {
21051				$scope.showChecks = key;
21052			}
21053			$scope.closeChecks = function() {
21054				$scope.showChecks = null;
21055			}
21056
21057			$scope.selectColumnType = function(column) {
21058				$angular.openModal('panel-column-types', {
21059					column: column,
21060					size: 'lg'
21061				});
21062			}
21063
21064			$scope.hasColumns = function(column) {
21065				if (column.columns && column.columns.length > 0) {
21066					return true;
21067				}
21068				if (column.type == 'group' || column.type == 'relation_entity' || column.type == 'relation' || column.type == 'table') {
21069					return true;
21070				}
21071				if ($scope.entity == 'form' && (column.type == 'inner_entity' || column.type == 'inner_entity_list')) {
21072					return true;
21073				}
21074				return false;
21075			};
21076
21077			$scope.getEntities = function() {
21078				if ($scope.entity === 'spreadsheet') {
21079					return $scope.model.entities ? $scope.model.entities : [];
21080				
21081				} else if ($scope.entity === 'synchronizer') {
21082					return $scope.model.entity_to_sync ? [$scope.model.entity_to_sync] : [];
21083				
21084				} else if ($scope.model.root_entity) {
21085					return [$scope.model.root_entity];
21086				
21087				} else if ($angular.contentScope.entityId) {
21088					return [$angular.contentScope.entityId];
21089				}
21090				return [];
21091			};
21092
21093			$scope.getInnerEntities = function() {
21094				var innerEntities = [];
21095				$scope.getEntities().forEach(function(entityId) {
21096					var e = $entity.getEntity(entityId);
21097					if (e && e.inner_entities) {
21098						e.inner_entities.forEach(function(innerEntity) {
21099							if (innerEntities.indexOf(innerEntity) < 0) {
21100								innerEntities.push(innerEntity);
21101							}
21102						});
21103					}
21104				});
21105				return innerEntities;
21106			};
21107			
21108			$scope.getAutoPathEntity = function() {
21109				var entity = $scope.model.param;
21110				if ($utils.isBlank(entity)) {
21111					entity = self.autopathRootEntity;
21112				}
21113				if ($utils.isBlank(entity)) {
21114					entity = $scope.getEntities().join(',');
21115				}
21116				return entity; 
21117			};
21118			
21119			$scope.isShowDoc = false;
21120			$scope.showDoc = function() {
21121				$scope.isShowDoc = !$scope.isShowDoc; 
21122			};
21123
21124			$scope.buildFormula = function(column) {
21125				var entity = $scope.getAutoPathEntity();
21126				if (entity && entity.indexOf(',') >= 0) {
21127					entity = entity.split(',')[0];
21128				}
21129				$angular.openModalLocal('panel-formula-builder', {
21130					size: 'xl',
21131					entity: entity,
21132					formula: column.formula
21133				}).then(function(result) {
21134					column.formula = result;
21135				});
21136			};
21137		}
21138	}
21139})();

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.