PluginProbe ʕ •ᴥ•ʔ
Event Tickets with Ticket Scanner / 3.1.2
Event Tickets with Ticket Scanner v3.1.2
3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 trunk 2.6.0 2.7.0 2.7.1 2.7.10 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.10 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 2.9.8 2.9.9 3.0.0 3.0.1 3.0.2 3.0.3
event-tickets-with-ticket-scanner / backend.js
event-tickets-with-ticket-scanner Last commit date
3rd 1 week ago css 1 week ago img 1 week ago includes 1 week ago js 1 week ago languages 1 week ago ticket 1 week ago vendors 1 week ago SASO_EVENTTICKETS.php 1 week ago backend.js 1 week ago changelog-features.json 1 week ago changelog.txt 1 week ago db.php 1 week ago index.php 1 week ago init_file.php 1 week ago order_details.js 1 week ago pwa-sw.js 1 week ago readme.txt 1 week ago saso-eventtickets-validator.js 1 week ago sasoEventtickets_AdminSettings.php 1 week ago sasoEventtickets_Authtoken.php 1 week ago sasoEventtickets_Base.php 1 week ago sasoEventtickets_Core.php 1 week ago sasoEventtickets_Frontend.php 1 week ago sasoEventtickets_Messenger.php 1 week ago sasoEventtickets_Options.php 1 week ago sasoEventtickets_PDF.php 1 week ago sasoEventtickets_Seating.php 1 week ago sasoEventtickets_Ticket.php 1 week ago sasoEventtickets_TicketBadge.php 1 week ago sasoEventtickets_TicketDesigner.php 1 week ago sasoEventtickets_TicketQR.php 1 week ago ticket_events.js 1 week ago ticket_scanner.js 1 week ago validator.js 1 week ago version-notices.json 1 week ago vollstart-cross-promo.php 1 week ago wc_backend.js 1 week ago wc_frontend.js 1 week ago woocommerce-hooks.php 1 week ago
backend.js
6297 lines
1 function sasoEventtickets(_myAjaxVar, doNotInit) {
2 const { __, _x, _n, sprintf } = wp.i18n;
3 let myAjax = _myAjaxVar;
4 let self = this;
5 let PREMIUM = null;
6 var $ = jQuery;
7 var PARAS = basics_ermittelURLParameter();
8 var DATA = {
9 /*action: '',*/
10 nonce: myAjax.nonce,
11 last_nonce_check: 0
12 };
13
14 var system = {is_debug:false, DYNJS:{}, DYNJS_CACHE:{}};
15 var FATAL_ERROR = false;
16 var DIV = null;
17 var LAYOUT = null;
18 var DATA_LISTS = null;
19 var DATA_AUTHTOKENS = null;
20 var OPTIONS = {
21 list:[], mapKeys:{},
22 versions:{mapKeys:{}},
23 meta_tags_keys:{list:[], mapKeys:{}},
24 infos:{},
25 tickets_for_testing:[],
26 options_special:{},
27 dismissed_suggestions:[]
28 };
29
30 var STATE = null;
31
32 if (_myAjaxVar._doNotInit) doNotInit = true;
33
34 function time() {
35 return new Date().getTime();
36 }
37
38 // Format activation timestamp ("YYYY-MM-DD HH:MM:SS" or with trailing "~" for estimate)
39 // to a localized date. The "~" suffix marks values derived from filesystem mtime
40 // rather than a real activation event — they're shown with "(estimate)" hint.
41 function _formatActivationDate(s) {
42 if (!s) return '';
43 var isEstimate = s.slice(-1) === '~';
44 if (isEstimate) s = s.slice(0, -1);
45 var d = new Date(s.replace(' ', 'T') + 'Z');
46 if (isNaN(d.getTime())) return s + (isEstimate ? ' (estimate)' : '');
47 var dateStr = d.toLocaleDateString();
48 return dateStr + (isEstimate ? ' (estimate)' : '');
49 }
50
51 function destroy_tags(t) {
52 if (t != null) {
53 t = t.replace("<", "").replace(">","");
54 }
55 return t;
56 }
57
58 function _requestURL(action, myData) {
59 let paras = '?action='+myAjax._action+'&a_sngmbh='+action+'&nonce='+ DATA.nonce+'&t='+time();
60 if (myData) {
61 for(let key in myData) paras += '&data['+key+']='+encodeURIComponent(myData[key]);
62 }
63 for(let key in DATA) paras += '&'+key+'='+encodeURIComponent(DATA[key]);
64 return myAjax.url + paras;
65 }
66
67 function _makePost(action, myData, cbf, ecbf, pcbf) {
68 if (FATAL_ERROR) return;
69 let _data = Object.assign({}, DATA);
70 _data.action = myAjax._action;
71 _data.a_sngmbh = action;
72 _data.t = new Date().getTime();
73 _data.nonce = DATA.nonce;
74 pcbf && pcbf();
75 for(var key in myData) _data['data['+key+']'] = myData[key];
76 // Pass through debug parameter if set in URL
77 var urlParams = new URLSearchParams(window.location.search);
78 if (urlParams.has('VollstartValidatorDebug')) {
79 _data['VollstartValidatorDebug'] = urlParams.get('VollstartValidatorDebug') || '1';
80 }
81 $.post( myAjax.url, _data, function( response ) {
82 if (response && response.data && response.data.nonce) {
83 DATA.last_nonce_check = new Date().getTime();
84 DATA.nonce = response.data.nonce;
85 }
86 if (!response.success) {
87 if (ecbf) ecbf(response);
88 else LAYOUT.renderFatalError(response.data);
89 } else {
90 cbf && cbf(response.data);
91 }
92 }).fail(function(jqXHR, textStatus) {
93 if (ecbf) ecbf({success:false, data:textStatus});
94 else LAYOUT.renderFatalError(textStatus);
95 });
96 }
97
98 function _makeGet(action, myData, cbf, ecbf, pcbf) {
99 if (FATAL_ERROR) return;
100 let _data = Object.assign({}, DATA);
101 _data.action = myAjax._action;
102 _data.a_sngmbh = action;
103 _data.t = new Date().getTime();
104 _data.nonce = DATA.nonce;
105 pcbf && pcbf();
106 for(var key in myData) _data['data['+key+']'] = myData[key];
107 // Pass through debug parameter if set in URL
108 var urlParams = new URLSearchParams(window.location.search);
109 if (urlParams.has('VollstartValidatorDebug')) {
110 _data['VollstartValidatorDebug'] = urlParams.get('VollstartValidatorDebug') || '1';
111 }
112 $.get( myAjax.url, _data, function( response ) {
113 if (response && response.data && response.data.nonce) {
114 DATA.last_nonce_check = new Date().getTime();
115 DATA.nonce = response.data.nonce;
116 }
117 if (!response.success) {
118 if (ecbf) ecbf(response);
119 else LAYOUT.renderFatalError(response.data);
120 } else {
121 cbf && cbf(response.data);
122 }
123 }).fail(function(jqXHR, textStatus) {
124 if (ecbf) ecbf({success:false, data:textStatus});
125 else LAYOUT.renderFatalError(textStatus);
126 });
127 }
128
129 function getOptionsFromServer(cbf, ecbf, pcbf) {
130 _makeGet('getOptions', {}, options=>{
131 _setOptions(options);
132 cbf && cbf(options);
133 }, ecbf, pcbf);
134 }
135
136 function _downloadFile(action, myData, filenameToStore, cbf, ecbf, pcbf) {
137 let _data = Object.assign({}, DATA);
138 _data.action = myAjax._action;
139 _data.a_sngmbh = action;
140 _data.t = new Date().getTime();
141 _data.nonce = DATA.nonce;
142 pcbf && pcbf();
143 for(var key in myData) _data['data['+key+']'] = myData[key];
144 let params = "";
145 for(var key in _data) params += key+"="+_data[key]+"&";
146 let url = myAjax.url+'?'+params;
147 let window_name = myData.code ? myData.code : '_blank';
148 let new_window = window.open(url, window_name);
149 //window.location.href = url;
150 //ajax_downloadFile(url, filenameToStore, cbf);
151 }
152 function ajax_downloadFile(urlToSend, fileName, cbf) {
153 var req = new XMLHttpRequest();
154 req.open("GET", urlToSend, true);
155 req.responseType = "blob";
156 req.onload = function (event) {
157 var blob = req.response;
158 //var fileName = req.getResponseHeader("X-fileName") //if you have the fileName header available
159 var link=document.createElement('a');
160 link.href=window.URL.createObjectURL(blob);
161 link.download=fileName;
162 link.click();
163 cbf && cbf();
164 };
165
166 req.send();
167 }
168
169 function speakOutLoud(v, display) {
170 if ('speechSynthesis' in window) {
171 var t = typeof v === 'object' ? 'Value is an object.' : v;
172 if (t.trim() == "") t = 'Value is empty';
173 var msg = new SpeechSynthesisUtterance(t);
174 msg.lang = "en-US";
175 window.speechSynthesis.speak(msg);
176 if (display) console.log("Speak:", v);
177 } else {
178 console.log(v);
179 }
180 }
181 function _saveOptionValue(key, value, cbf, pcbf) {
182 _makePost('changeOption', {'key':key, 'value':value},
183 ()=>{
184 cbf && cbf();
185 if (key == "wcTicketDesignerTemplateTest") {
186 $("#wcTicketDesignerTemplateTest_button_PDF").prop("disabled", false).text(__('Preview Test Template Code as PDF', 'event-tickets-with-ticket-scanner'));
187 }
188 if (key == "serial") {
189 // Hide license-related admin banners immediately — server has suppressed
190 // them for 60s via transient, but JS fadeOut gives instant visual feedback
191 // without waiting for page reload.
192 let $serialValue = typeof value === 'string' ? value.trim() : '';
193 if ($serialValue !== '') {
194 $('.saso-license-banner').fadeOut(400);
195 }
196
197 // Immediately recheck license after serial change
198 let $statusEl = $('[data-key="serial"]').parent().find('.saso-license-inline-status');
199 if ($statusEl.length === 0) {
200 $statusEl = $('<span class="saso-license-inline-status" style="margin-left:10px;">');
201 $('[data-key="serial"]').after($statusEl);
202 }
203 $statusEl.html('<i>'+__('Checking license...', 'event-tickets-with-ticket-scanner')+'</i>');
204 _makePost('recheckLicense', {}, function(result) {
205 if (result) {
206 let color = result.active ? 'green' : 'red';
207 let label = result.active ? __('Active', 'event-tickets-with-ticket-scanner') : __('Inactive', 'event-tickets-with-ticket-scanner');
208 if (result.subscription_type === 'lifetime') label += ' (Lifetime)';
209 $statusEl.html('<span style="color:'+color+';font-weight:bold;">'+label+'</span>');
210 }
211 // ALWAYS reload after a serial save — the backend may have swapped
212 // Starter → Premium during save, and the page state needs to refresh
213 // to reflect the new plugin. Reloading regardless of active/inactive
214 // ensures users never get stuck with a stale banner.
215 if ($serialValue !== '') {
216 setTimeout(function() { location.reload(); }, 2000);
217 }
218 }, function() {
219 $statusEl.html('<span style="color:red;">'+__('Check failed', 'event-tickets-with-ticket-scanner')+'</span>');
220 // Reload anyway — save succeeded, recheck just failed (network/server)
221 if ($serialValue !== '') {
222 setTimeout(function() { location.reload(); }, 3000);
223 }
224 });
225 if (_getOptions_Versions_getByKey('isOldPremiumDetected')) {
226 __checkPremiumUpdateAfterSerial(value);
227 }
228 }
229 }, null,
230 ()=>{
231 pcbf && pcbf();
232 if (key == "wcTicketDesignerTemplateTest") {
233 $("#wcTicketDesignerTemplateTest_button_PDF").prop("disabled", true).text(__('saving...', 'event-tickets-with-ticket-scanner'));
234 }
235 });
236
237 }
238
239 function _setOptions(optionData) {
240 OPTIONS.list = optionData.options;
241 for (let a=0;a<OPTIONS.list.length;a++) {
242 let item = OPTIONS.list[a];
243 OPTIONS.mapKeys[item.key] = item;
244 OPTIONS.mapKeys[item.key].getValue = function(key) {
245 return function() {return _getOptions_getValByKey(key);};
246 }(item.key);
247 }
248 if (optionData.versions) {
249 if (!optionData.versions.IS_PRETTY_PERMALINK_ACTIVATED) {
250 LAYOUT.renderInfoBox(__("Warning", 'event-tickets-with-ticket-scanner'), __("In order to make the ticket detail view and the ticket scanner work, you need to set a permalink structure within the settings.<br>Please go to the settings->permalinks and choose a permalink structure, that is not 'plain'.", 'event-tickets-with-ticket-scanner'));
251 }
252 OPTIONS.versions.mapKeys = optionData.versions;
253 }
254 system.is_debug = typeof optionData.versions.is_debug != "undefined" && optionData.versions.is_debug == 1 ? true : false;
255 if (optionData.meta_tags_keys) {
256 OPTIONS.meta_tags_keys.list = optionData.meta_tags_keys;
257 OPTIONS.meta_tags_keys.mapKeys = {};
258 for (let a=0;a<OPTIONS.meta_tags_keys.list.length;a++) {
259 let item = OPTIONS.meta_tags_keys.list[a];
260 OPTIONS.meta_tags_keys.mapKeys[item.key] = item;
261 OPTIONS.meta_tags_keys.mapKeys[item.key].getValue = function(key) {
262 return function() {return _getOptions_Meta_getValByKey(key);};
263 }(item.key);
264 }
265 }
266 if (optionData.infos) {
267 OPTIONS.infos = optionData.infos;
268 }
269 if (optionData.tickets_for_testing) {
270 OPTIONS.tickets_for_testing = optionData.tickets_for_testing;
271 }
272 if (optionData.options_special) {
273 OPTIONS.options_special = optionData.options_special;
274 }
275 OPTIONS.dismissed_suggestions = optionData.dismissed_suggestions || [];
276
277 let _hasPremiumVersion = _getOptions_Versions_getByKey('premium') != '';
278 if (_hasPremiumVersion) {
279 let serial = _getOptions_getValByKey('serial');
280 // 24h dismiss: clicking "Later" stores a localStorage timestamp
281 // so the modal stays quiet for a day instead of nagging on every
282 // admin page load. Customer can still enter the key on the
283 // Options page anytime — the field is rendered there.
284 let licenseDismissedUntil = 0;
285 try {
286 licenseDismissedUntil = parseInt(localStorage.getItem('saso_et_license_modal_dismissed_until') || '0', 10) || 0;
287 } catch (e) { /* private mode, no localStorage — fall through */ }
288 let dismissNow = Date.now();
289 if (serial == '' && licenseDismissedUntil > dismissNow) {
290 // quiet — fall through
291 } else if (serial == '') {
292 if (STATE != "options") {
293 let dlgContent = $('<div/>');
294 dlgContent.append('<p>'+__('Thank you for using the Premium version!', 'event-tickets-with-ticket-scanner')+'</p>');
295 dlgContent.append('<p>'+__('Please enter your license key to activate updates and premium features.', 'event-tickets-with-ticket-scanner')+'</p>');
296 let serialInput = $('<input type="text" style="width:100%;padding:8px;font-size:14px;border:1px solid #ccc;border-radius:4px;" placeholder="XXXX-XXXX-XXXX-XXXX"/>');
297 dlgContent.append(serialInput);
298 let statusDiv = $('<div/>').css({"margin-top":"10px","display":"none"});
299 dlgContent.append(statusDiv);
300 dlgContent.dialog({
301 title: __('Premium License Key', 'event-tickets-with-ticket-scanner'),
302 modal: true,
303 width: 450,
304 dialogClass: "no-close",
305 open: function() { setTimeout(function(){ serialInput.focus(); }, 100); },
306 buttons: [
307 {
308 text: __('Activate', 'event-tickets-with-ticket-scanner'),
309 class: "button-primary",
310 click: function() {
311 let $dlg = $(this);
312 let key = serialInput.val().trim();
313 if (key === '') {
314 statusDiv.html('<span style="color:red;">'+__('Please enter a license key.', 'event-tickets-with-ticket-scanner')+'</span>').show();
315 return;
316 }
317 // Lock UI — big spinner, disable everything so user waits
318 $dlg.parent().find('.ui-dialog-buttonpane button').prop('disabled', true).css({opacity: 0.5, cursor: 'not-allowed'});
319 $dlg.parent().find('.ui-dialog-buttonpane button.button-primary').text(__('Checking...', 'event-tickets-with-ticket-scanner'));
320 serialInput.prop('disabled', true).css('opacity', 0.5);
321 statusDiv.html(
322 '<div style="display:flex;align-items:center;justify-content:center;gap:10px;padding:14px;background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0">'
323 + '<span class="spinner is-active" style="float:none;margin:0;visibility:visible"></span>'
324 + '<span style="color:#334155;font-weight:500">'+__('Validating license — please wait…', 'event-tickets-with-ticket-scanner')+'</span>'
325 + '</div>'
326 ).show();
327 _makePost('changeOption', {key:'serial', value:key}, function() {
328 statusDiv.html(
329 '<div style="padding:14px;background:#ecfdf5;border-radius:8px;border:1px solid #a7f3d0;color:#065f46;font-weight:600;text-align:center">'
330 + '&#10003; '+__('License key saved. Reloading…', 'event-tickets-with-ticket-scanner')
331 + '</div>'
332 );
333 setTimeout(function(){ location.reload(); }, 1500);
334 }, function(err) {
335 $dlg.parent().find('.ui-dialog-buttonpane button').prop('disabled', false).css({opacity: 1, cursor: 'pointer'});
336 $dlg.parent().find('.ui-dialog-buttonpane button.button-primary').text(__('Activate', 'event-tickets-with-ticket-scanner'));
337 serialInput.prop('disabled', false).css('opacity', 1);
338 statusDiv.html('<span style="color:red;">'+__('Error:', 'event-tickets-with-ticket-scanner')+' '+(err && err.data ? err.data : 'unknown')+'</span>').show();
339 });
340 }
341 },
342 {
343 text: __('Later', 'event-tickets-with-ticket-scanner'),
344 class: "button-secondary",
345 click: function() {
346 try {
347 localStorage.setItem('saso_et_license_modal_dismissed_until', String(Date.now() + 24*60*60*1000));
348 } catch (e) { /* ignore */ }
349 $(this).dialog("close");
350 }
351 }
352 ]
353 });
354 }
355 }
356 if (serial != "" && typeof OPTIONS.infos.premium_expiration !== "undefined") {
357 let expiration = OPTIONS.infos.premium_expiration;
358 if (expiration.last_run != 0 && expiration.timestamp > 0) {
359 let expirationDate = new Date(expiration.timestamp * 1000);
360 let toCheck = new Date();
361 toCheck.setDate(toCheck.getDate() + 21);
362 let today = new Date();
363 if (expirationDate <= today || toCheck >= expirationDate) {
364 let msg = typeof expiration.message !== "undefined" && expiration.message != "" ? '<br>'+expiration.message : '';
365
366 // Only warn for subscriptions, NOT for lifetime/onetime licenses
367 // Lifetime licenses continue working with Basic < 2.8.0
368 let subType = expiration.subscription_type || '';
369 let isLifetime = subType === 'lifetime' || subType === 'onetime' || !subType;
370
371 if (!isLifetime) {
372 // Monthly or Yearly subscription - Premium will STOP
373 let isMonthly = subType.toLowerCase().includes('month');
374 let title, bodyText;
375
376 if (isMonthly) {
377 title = "Your monthly subscription payment is due soon!";
378 bodyText = "Your premium license will be <strong>disabled</strong> if the payment fails on "+expiration.expiration_date+ ' '+expiration.timezone+'.<br>Please ensure your payment method is up to date.<br>'+msg+'After payment failure, the plugin will revert to Basic features only.<br>You can manage your subscription in your <a target=\"_blank\" style=\"color:white;font-weight:bold;\" href=\"https://vollstart.com/event-tickets-with-ticket-scanner/\">account settings</a>.';
379 } else {
380 title = "Your premium license expires soon!";
381 bodyText = "Your premium license will be <strong>disabled</strong> on "+expiration.expiration_date+ ' '+expiration.timezone+'.<br>It will revert to Basic features only after expiration.<br>'+msg+'You can <a target=\"_blank\" style=\"color:white;font-weight:bold;\" href=\"https://vollstart.com/event-tickets-with-ticket-scanner/\">renew your premium license here</a>.';
382 }
383
384 let info_box = $('<div style="background-color:#dc3232;color:white;padding:10px;border-left:4px solid #dc3232;">').html("<strong>"+title+"</strong><br>"+bodyText);
385 $('body').find('div[data-id="plugin_info_area"]').html(info_box);
386 }
387 }
388 }
389 }
390 }
391 }
392
393 function _getOptions_getByKey(key) {
394 if (OPTIONS.mapKeys[key]) return OPTIONS.mapKeys[key];
395 return null;
396 }
397 function _getOptions_Meta_getByKey(key) {
398 if (OPTIONS.meta_tags_keys.mapKeys[key]) return OPTIONS.meta_tags_keys.mapKeys[key];
399 return null;
400 }
401 function _getOptions_Versions_getByKey(key) {
402 if (OPTIONS.versions.mapKeys[key]) return OPTIONS.versions.mapKeys[key];
403 return null;
404 }
405 function _getOptions_Infos_getByKey(key) {
406 if (OPTIONS.infos[key]) return OPTIONS.infos[key];
407 return null;
408 }
409 function _getOptions_isActivatedByKey(key) {
410 let po = _getOptions_getByKey(key);
411 if (po == null) return false;
412 return po.value == 1;
413 }
414 function _getOptions_Versions_isActivatedByKey(key) {
415 let po = _getOptions_Versions_getByKey(key);
416 if (po == null) return false;
417 return po == 1;
418 }
419 function _getOptions_getLabelByKey(key) {
420 let po = _getOptions_getByKey(key);
421 if (po == null) return "";
422 return po.label;
423 }
424 function _getOptions_Meta_getLabelByKey(key) {
425 let po = _getOptions_Meta_getByKey(key);
426 if (po == null) return "";
427 return po.label;
428 }
429 function _getOptions_getValByKey(key) {
430 let po = _getOptions_getByKey(key);
431 if (po == null) return "";
432 return po.value == "" ? po['default'] : po.value;
433 }
434 function _getOptions_Versions_getValByKey(key) {
435 let po = _getOptions_Versions_getByKey(key);
436 if (po == null) return "";
437 return po;
438 }
439
440 function basics_ermittelURLParameter() {
441 var parawerte = {};
442 var teile;
443 if (window.location.search !== "") {
444 teile = window.location.search.substring(1).split("&");
445 for (var a=0;a<teile.length;a++)
446 {
447 var pos = teile[a].indexOf("=");
448 if (pos < 0) {
449 parawerte[teile[a]] = true;
450 } else {
451 var key = teile[a].substring(0,pos);
452 parawerte[key] = decodeURIComponent(teile[a].substring(pos+1));
453 }
454 }
455 }
456 return parawerte;
457 }
458
459 function intval(v) {
460 let retv = parseInt(v,10);
461 if (isNaN(retv)) retv = 0;
462 return retv;
463 }
464
465 function getDefaultDateFormat() {
466 return (OPTIONS?.options_special?.format_date) ? OPTIONS.options_special.format_date : "d.m.Y";
467 }
468 function getDefaultDateTimeFormat() {
469 return OPTIONS.options_special.format_datetime ? OPTIONS.options_special.format_datetime : "d.m.Y H:i";
470 }
471 function DateTime2Text(millisek) {
472 return Date2Text(millisek, getDefaultDateTimeFormat());
473 }
474 /*
475 function Date2Text(millisek, format, timezone_id) {
476 if (!timezone_id) timezone_id = _getOptions_Versions_getByKey("date_WP_timezone");
477 if (!millisek)
478 millisek = time(timezone_id);
479 var d = new Date(millisek);
480 if (!format)
481 //format = system.format_date ? system.format_date : "%d.%m.%Y";
482 format = getDefaultDateFormat();
483 //format = "%d.%m.%Y %H:%i";
484 var tage = [
485 _x('Sun', 'cal', 'event-tickets-with-ticket-scanner'),
486 _x('Mon', 'cal', 'event-tickets-with-ticket-scanner'),
487 _x('Tue', 'cal', 'event-tickets-with-ticket-scanner'),
488 _x('Wed', 'cal', 'event-tickets-with-ticket-scanner'),
489 _x('Thu', 'cal', 'event-tickets-with-ticket-scanner'),
490 _x('Fri', 'cal', 'event-tickets-with-ticket-scanner'),
491 _x('Sat', 'cal', 'event-tickets-with-ticket-scanner')
492 ];
493 var monate = [
494 _x('Jan', 'cal', 'event-tickets-with-ticket-scanner'),
495 _x('Feb', 'cal', 'event-tickets-with-ticket-scanner'),
496 _x('Mar', 'cal', 'event-tickets-with-ticket-scanner'),
497 _x('Apr', 'cal', 'event-tickets-with-ticket-scanner'),
498 _x('May', 'cal', 'event-tickets-with-ticket-scanner'),
499 _x('Jun', 'cal', 'event-tickets-with-ticket-scanner'),
500 _x('Jul', 'cal', 'event-tickets-with-ticket-scanner'),
501 _x('Aug', 'cal', 'event-tickets-with-ticket-scanner'),
502 _x('Sep', 'cal', 'event-tickets-with-ticket-scanner'),
503 _x('Oct', 'cal', 'event-tickets-with-ticket-scanner'),
504 _x('Nov', 'cal', 'event-tickets-with-ticket-scanner'),
505 _x('Dec', 'cal', 'event-tickets-with-ticket-scanner')
506 ];
507 var formate = {'d':d.getDate()<10?'0'+d.getDate():d.getDate(),
508 'j':d.getDate(),'D':tage[d.getDay()],'w':d.getDate(),'m':d.getMonth()+1<10?'0'+(d.getMonth()+1):d.getMonth()+1,'M':monate[d.getMonth()],
509 'n':d.getMonth()+1,'Y':d.getFullYear(),'y':d.getYear()>100?d.getYear().toString().substring(d.getYear().toString().length-2):d.getYear(),
510 'H':d.getHours()<10?'0'+d.getHours():d.getHours(),'h':d.getHours()>12?d.getHours()-12:d.getHours(),
511 'i':d.getMinutes()<10?'0'+d.getMinutes():d.getMinutes(),'s':d.getSeconds()<10?'0'+d.getSeconds():d.getSeconds()
512 };
513 for (var akey in formate) {
514 //var rg = new RegExp('%'+akey, "g");
515 var rg = new RegExp(akey, "g");
516 format = format.replace(rg, formate[akey]);
517 }
518 return format;
519 }
520 */
521
522 function DateFormatStringToDateTimeText(datestring, format, timezone_id) {
523 if (!format) format = getDefaultDateTimeFormat();
524 let millisek = parseToMillis(datestring, timezone_id);
525 return Date2Text(millisek, format, timezone_id);
526 }
527 function DateFormatStringToDateText(datestring, format, timezone_id) {
528 let millisek = parseToMillis(datestring, timezone_id);
529 return Date2Text(millisek, format, timezone_id);
530 }
531
532 function Date2Text(millisek, format, timezone_id) {
533 // 1) Timezone bestimmen (Fallback: UTC)
534 if (!timezone_id) {
535 timezone_id = _getOptions_Versions_getByKey("date_WP_timezone") || "UTC";
536 }
537
538 // 2) Timestamp normalisieren (PHP liefert oft Sekunden; JS braucht Millisekunden)
539 if (typeof millisek === "string") millisek = Number(millisek);
540 if (!millisek) {
541 // Deine bestehende Logik – falls du hier einen Unix-TS in Sekunden bekommst, bitte ggf. *1000 ergänzen
542 millisek = time(timezone_id);
543 }
544 if (String(Math.trunc(millisek)).length === 10) {
545 millisek = millisek * 1000;
546 }
547 const date = new Date(Number(millisek));
548
549 // 3) Defaults für Format
550 if (!format) {
551 format = getDefaultDateFormat();
552 }
553
554 // 4) Lokalisierte Kurzformen (nutzt deine _x-Übersetzungen)
555 const tage = [
556 _x('Sun', 'cal', 'event-tickets-with-ticket-scanner'),
557 _x('Mon', 'cal', 'event-tickets-with-ticket-scanner'),
558 _x('Tue', 'cal', 'event-tickets-with-ticket-scanner'),
559 _x('Wed', 'cal', 'event-tickets-with-ticket-scanner'),
560 _x('Thu', 'cal', 'event-tickets-with-ticket-scanner'),
561 _x('Fri', 'cal', 'event-tickets-with-ticket-scanner'),
562 _x('Sat', 'cal', 'event-tickets-with-ticket-scanner')
563 ];
564 const monate = [
565 _x('Jan', 'cal', 'event-tickets-with-ticket-scanner'),
566 _x('Feb', 'cal', 'event-tickets-with-ticket-scanner'),
567 _x('Mar', 'cal', 'event-tickets-with-ticket-scanner'),
568 _x('Apr', 'cal', 'event-tickets-with-ticket-scanner'),
569 _x('May', 'cal', 'event-tickets-with-ticket-scanner'),
570 _x('Jun', 'cal', 'event-tickets-with-ticket-scanner'),
571 _x('Jul', 'cal', 'event-tickets-with-ticket-scanner'),
572 _x('Aug', 'cal', 'event-tickets-with-ticket-scanner'),
573 _x('Sep', 'cal', 'event-tickets-with-ticket-scanner'),
574 _x('Oct', 'cal', 'event-tickets-with-ticket-scanner'),
575 _x('Nov', 'cal', 'event-tickets-with-ticket-scanner'),
576 _x('Dec', 'cal', 'event-tickets-with-ticket-scanner')
577 ];
578
579 // 5) Teile in gewünschter Timezone extrahieren
580 const dtf = new Intl.DateTimeFormat("de-CH", {
581 timeZone: timezone_id,
582 year: "numeric",
583 month: "2-digit",
584 day: "2-digit",
585 hour: "2-digit",
586 minute: "2-digit",
587 second: "2-digit",
588 weekday: "short",
589 hour12: false
590 });
591 const parts = Object.fromEntries(dtf.formatToParts(date).map(p => [p.type, p.value]));
592
593 const monthNum = Number(parts.month); // 1..12 (als "01".."12")
594 const dayNum = Number(parts.day); // 1..31
595 const hourNum = Number(parts.hour); // 0..23
596
597 // Wochentag-Index (0=Sun..6=Sat) in der angegebenen Timezone
598 const weekdayEn = new Intl.DateTimeFormat("en-US", { timeZone: timezone_id, weekday: "short" }).format(date);
599 const weekdayIndex = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"].indexOf(weekdayEn);
600
601 // 6) Token-Mapping (PHP-ähnlich)
602 const formate = {
603 'd': parts.day, // 01..31
604 'j': String(dayNum), // 1..31
605 'D': tage[weekdayIndex], // So/Mo/... (aus _x oben, hier auf 'Sun'.. gemappt)
606 'w': String(weekdayIndex), // 0..6 (So=0)
607 'm': parts.month, // 01..12
608 'M': monate[monthNum - 1], // Jan..Dec (aus _x oben)
609 'n': String(monthNum), // 1..12
610 'Y': parts.year, // 2025
611 'y': parts.year.slice(-2), // 25
612 'H': parts.hour, // 00..23
613 'h': String(((hourNum % 12) || 12)).padStart(2,'0'), // 01..12
614 'i': parts.minute, // 00..59
615 's': parts.second // 00..59
616 };
617
618 // 7) Token ersetzen (ohne %; entspricht deiner aktuellen Logik)
619 for (const akey in formate) {
620 const rg = new RegExp(akey, "g");
621 format = format.replace(rg, formate[akey]);
622 }
623 return format;
624 }
625
626 // Hilfsfunktion: Offset-Minuten einer IANA-Zeitzone für einen UTC-Instant ermitteln.
627 // Nutzt Intl.DateTimeFormat mit timeZoneName:'shortOffset' (z.B. "GMT+2").
628 function _getTzOffsetMinutes(utcDate, timezone_id) {
629 const fmt = new Intl.DateTimeFormat('en-US', {
630 timeZone: timezone_id,
631 timeZoneName: 'shortOffset',
632 year: 'numeric', month: '2-digit', day: '2-digit',
633 hour: '2-digit', minute: '2-digit', second: '2-digit',
634 hour12: false
635 });
636 const parts = fmt.formatToParts(utcDate);
637 const z = parts.find(p => p.type === 'timeZoneName')?.value || 'GMT+0';
638 // Erwartete Form: "GMT+2" oder "GMT+02:00"
639 const m = z.match(/GMT([+-])(\d{1,2})(?::?(\d{2}))?/i);
640 if (!m) return 0;
641 const sign = m[1] === '-' ? -1 : 1;
642 const hours = parseInt(m[2], 10);
643 const mins = m[3] ? parseInt(m[3], 10) : 0;
644 return sign * (hours * 60 + mins);
645 }
646
647 // Wandelt verschiedenste Eingaben in einen UTC-Millis-Timestamp.
648 // - Zahlen (Sekunden/Millis) -> normalisiert
649 // - ISO-Strings mit Z/±hh:mm -> nativ geparst
650 // - Naive Strings (z.B. "YYYY-MM-DD HH:mm:ss") -> als timezone_id-Wandzeit interpretiert
651 function parseToMillis(input, timezone_id) {
652 timezone_id = timezone_id || (typeof _getOptions_Versions_getByKey === 'function'
653 ? _getOptions_Versions_getByKey("date_WP_timezone") : "UTC");
654
655 // 1) Direkt Number?
656 if (typeof input === 'number') {
657 // 10-stellige Sekunden -> *1000
658 if (String(Math.trunc(input)).length === 10) return input * 1000;
659 return input; // bereits Millisekunden
660 }
661
662 // 2) String -> trim
663 if (typeof input === 'string') {
664 const s = input.trim();
665
666 // 2a) Reine Ziffern -> Sekunden/Millis
667 if (/^\d+$/.test(s)) {
668 const n = Number(s);
669 return (s.length === 10) ? n * 1000 : n;
670 }
671
672 // 2b) ISO mit Z / Offset -> nativ (sicher)
673 if (/T.*(Z|[+-]\d{2}:\d{2})$/.test(s)) {
674 const d = new Date(s);
675 if (!isNaN(d)) return d.getTime();
676 }
677
678 // 2c) Naive Formate: "YYYY-MM-DD HH:mm:ss" | "YYYY/MM/DD HH:mm" | "YYYY-MM-DD"
679 // Wir parsen Komponenten und interpretieren sie als Wandzeit in timezone_id.
680 const m = s.match(
681 /^(\d{4})[-\/](\d{2})[-\/](\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/
682 );
683 if (m) {
684 const Y = parseInt(m[1], 10);
685 const Mo = parseInt(m[2], 10);
686 const D = parseInt(m[3], 10);
687 const H = m[4] ? parseInt(m[4], 10) : 0;
688 const I = m[5] ? parseInt(m[5], 10) : 0;
689 const S = m[6] ? parseInt(m[6], 10) : 0;
690
691 // Instant-Kandidat in UTC aus den "lokalen" Komponenten
692 // Idee: Komponenten als UTC annehmen -> Offset der Ziel-Zeitzone abziehen.
693 const utcGuess = new Date(Date.UTC(Y, Mo - 1, D, H, I, S));
694
695 // Offset der Ziel-Zone zum angegebenen Zeitpunkt holen (inkl. DST)
696 const offMin = _getTzOffsetMinutes(utcGuess, timezone_id);
697
698 // Echte UTC-Millis, wenn Y-M-D H:I:S die Wandzeit in timezone_id ist:
699 return utcGuess.getTime() - offMin * 60 * 1000;
700 }
701
702 // 2d) Fallback: Versuch natives Date (Browser-lokal) – nicht ideal, aber besser als NaN
703 const d = new Date(s.replace(' ', 'T'));
704 if (!isNaN(d)) return d.getTime();
705 }
706
707 // 3) Wenn alles fehlschlägt -> NaN (oder wirf Fehler je nach Policy)
708 return NaN;
709 }
710 function _getMediaData(mediaid, cbf) {
711 _makeGet('getMediaData', {'mediaid':mediaid}, (ret)=>{
712 cbf && cbf(ret);
713 });
714 }
715
716 function getDataLists(cbf) {
717 if (DATA_LISTS !== null) cbf && cbf();
718 _makeGet('getLists', {}, data=>{
719 DATA_LISTS = data;
720 __updateFirstStepsProgress();
721 cbf && cbf(DATA_LISTS);
722 });
723 }
724
725 function getCodeObjectMeta(codeObj) {
726 if (codeObj.metaObj) return codeObj.metaObj;
727 try {
728 if (typeof codeObj.meta == "undefined" || codeObj.meta == "") {
729 codeObj.metaObj = null;
730 } else {
731 codeObj.metaObj = JSON.parse(codeObj.meta);
732 }
733 } catch(e) {
734 // new empty tickets have no meta
735 //console.log("Error should not happen. Meta is broken. ", codeObj);
736 codeObj.metaObj = null;
737 }
738 return codeObj.metaObj;
739 }
740
741 function updateCodeObject(codeObj, newCodeObj) {
742 for(var prop in newCodeObj) {
743 codeObj[prop] = newCodeObj[prop];
744 }
745 codeObj.metaObj = null;
746 }
747
748 function closeDialog(dlg) {
749 try {
750 $(dlg).dialog("close");
751 } catch(e) {}
752 $(dlg).html('');
753 try {
754 $(dlg).dialog("destroy");
755 } catch(e) {}
756 $(dlg).remove();
757 }
758
759 function getUseFulVideosHTML() {
760 return '<h3>Useful videos</h3><ul><li><span class="dashicons dashicons-external"></span><a href="https://youtu.be/yJcHMV7oAFc" target="_blank">Setup for use case Event Organizer (Youtube)</a></li><li><span class="dashicons dashicons-external"></span><a href="https://www.youtube.com/watch?v=TDMWI0R_HXQ" target="_blank">Setup for use case Club, Spa and Fitness clubs (Youtube)</a></li></ul>';
761 }
762
763 function getAuthtokens(cbf) {
764 if (DATA_AUTHTOKENS !== null) cbf && cbf();
765 _makeGet('getAuthtokens', {}, data=>{
766 DATA_AUTHTOKENS = data;
767 cbf && cbf(DATA_AUTHTOKENS);
768 });
769 }
770
771 function _displayAuthTokensArea() {
772 STATE = 'authtokens';
773 DIV.html('');
774 DIV.append(getBackButtonDiv());
775
776 DIV.append('<h3>'+_x('Auth Token', 'label', 'event-tickets-with-ticket-scanner')+'</h3>');
777 $('<p>').html(__('You can add auth tokens, that can be used to access your ticket scanner. Create an auth token and pass the QR code to the user or let the user scan it from your admin area. The used auth token will bypass any access restriction settings for the ticket scanner that are set in the options.', 'event-tickets-with-ticket-scanner')).appendTo(DIV);
778 $('<p>').html(__('The user scan the QR code for the auth token with the ticket scanner. Just like a normal ticket. The system will store the auth token to the browser.', 'event-tickets-with-ticket-scanner')).appendTo(DIV);
779 let loading = $('<div/>').html(_getSpinnerHTML()).appendTo(DIV);
780 let div2 = $('<div class="et-card">').appendTo(DIV);
781 let tplace = $('<div class="et-card"/>');
782
783 getOptionsFromServer(reply=>{
784 let tabelle_authtokens_datatable;
785 let btn_new = $('<button/>').addClass("button-primary").html(_x('Add', 'label', 'event-tickets-with-ticket-scanner')).on("click", ()=>{
786 __showMaskAuthtoken(null);
787 });
788 $('<div class="et-toolbar">').css('margin-bottom','10px').append(btn_new).appendTo(div2);
789 let div_tabelle = $('<div>');
790 loading.html("");
791 tplace.html("").append(div_tabelle).appendTo(div2);
792
793 function __showMaskAuthtoken(editValues) {
794 let _options = {
795 title: editValues !== null ? _x('Edit Auth Token', 'title', 'event-tickets-with-ticket-scanner') : _x('Add Auth Token', 'title', 'event-tickets-with-ticket-scanner'),
796 modal: true,
797 minWidth: 600,
798 minHeight: 400,
799 buttons: [
800 {
801 text: _x('Ok', 'label', 'event-tickets-with-ticket-scanner'),
802 click: function() {
803 ___submitForm();
804 }
805 },
806 {
807 text: _x('Cancel', 'label', 'event-tickets-with-ticket-scanner'),
808 click: function() {
809 closeDialog(this);
810 }
811 }
812 ]
813 };
814 let dlg = $('<div/>').html('<form>'+_x('Name', 'label', 'event-tickets-with-ticket-scanner')+'<br><input name="inputName" type="text" style="width:100%;" required></form>');
815 dlg.dialog(_options);
816
817 dlg.find("form").append('<p>'+_x('Bound to product(s)', 'label', 'event-tickets-with-ticket-scanner')+'<br><input name="inputBoundToProducts" type="text" placeholder="'+_x('all products allowed to be redeemed', 'label', 'event-tickets-with-ticket-scanner')+'" style="width:100%;"><br>'+__('You can add comma separated "," product ids. This will limit the user to redeem tickets only of products listed here. If left empty, all are allowed.', 'event-tickets-with-ticket-scanner')+'</p>');
818 dlg.dialog(_options);
819
820 dlg.find("form").append($('<p>'+_x('Description', 'label', 'event-tickets-with-ticket-scanner')+'<br><textarea name="desc" style="width:100%;"></textarea></p>'));
821 if (isPremium() && typeof PREMIUM.addAuthtokenMaskEditFields != "undefined") PREMIUM.addAuthtokenMaskEditFields(dlg, editValues);
822 dlg.find("form").append($('<p><input type="checkbox" name="aktiv">'+_x('is active', 'label', 'event-tickets-with-ticket-scanner')+'</p>'));
823
824 let form = dlg.find("form").on("submit", event=>{
825 event.preventDefault();
826 ___submitForm();
827 });
828
829 let metaObj = [];
830 if (editValues && typeof editValues.meta !== "undefined" && editValues.meta != "") {
831 try {
832 metaObj = JSON.parse(editValues.meta);
833 } catch(e) {}
834 }
835
836 if (editValues) {
837 form[0].elements['inputName'].value = editValues.name;
838 form[0].elements['inputName'].select();
839 form[0].elements['inputBoundToProducts'].value = editValues.metaObj.ticketscanner.bound_to_products;
840 form[0].elements['aktiv'].checked = editValues.aktiv == 1 ? true : false;
841 if (typeof metaObj.desc !== "undefined") {
842 form[0].elements['desc'].value = metaObj.desc;
843 }
844 }
845
846 function ___submitForm() {
847 let inputName = form[0].elements['inputName'].value.trim();
848 if (inputName === "") return;
849
850 dlg.html(_getSpinnerHTML());
851 let _data = {"name":inputName};
852 _data['aktiv'] = form[0].elements['aktiv'].checked ? 1 : 0;
853 _data['meta'] = {"desc":"", "ticketscanner":{"bound_to_products":""}};
854 _data['meta']['desc'] = form[0].elements['desc'].value.trim();
855 _data['meta']['ticketscanner']['bound_to_products'] = form[0].elements['inputBoundToProducts'].value.trim();
856 if (isPremium() && typeof PREMIUM.addAuthtokenMaskEditFieldsData != "undefined") PREMIUM.addAuthtokenMaskEditFieldsData(_data, form[0], editValues);
857
858 form[0].reset();
859 if (editValues) {
860 _data.id = editValues.id;
861 _makePost('editAuthtoken', _data, result=>{
862 DATA_AUTHTOKENS = null;
863 __renderTabelleAuthtokens();
864 //tabelle_authtokens_datatable.ajax.reload();
865 setTimeout(function(){closeDialog(dlg);},250);
866 }, ()=>{
867 closeDialog(dlg);
868 });
869 } else {
870 _makePost('addAuthtoken', _data, result=>{
871 DATA_AUTHTOKENS = null;
872 __renderTabelleAuthtokens();
873 closeDialog(dlg);
874 }, response=>{
875 closeDialog(dlg);
876 if (response.data.slice(0,1) === "#") {
877 FATAL_ERROR === false && LAYOUT.renderFatalError(response.data);
878 //FATAL_ERROR = true;
879 }
880 });
881 }
882 }
883 } // end __showMaskAuthtoken
884
885 function __renderTabelleAuthtokens() {
886 div_tabelle.html(_getSpinnerHTML());
887 getAuthtokens(()=>{
888 let table_id = myAjax.divPrefix+'_tabelle_authtokens';
889 let tabelle = $('<table/>').attr("id", table_id);
890 tabelle.html('<thead><tr><th></th><th align="left">'+_x('Name', 'label', 'event-tickets-with-ticket-scanner')+'</th><th align="left">'+_x('Created', 'label', 'event-tickets-with-ticket-scanner')+'</th><th>'+_x('Area', 'label', 'event-tickets-with-ticket-scanner')+'</th><th>'+_x('Status', 'label', 'event-tickets-with-ticket-scanner')+'</th><th></th></tr></thead>');
891 div_tabelle.html(tabelle);
892
893 let table = $('#'+table_id);
894 $(table).DataTable().clear().destroy();
895 tabelle_authtokens_datatable = $(table).DataTable({
896 "responsive": true,
897 "visible": true,
898 "searching": true,
899 "ordering": true,
900 "processing": true,
901 "serverSide": false,
902 "stateSave": true,
903 "data": DATA_AUTHTOKENS,
904 "order": [[ 1, "asc" ]],
905 "columns":[
906 {"data":null,"className":'details-control',"orderable":false,"defaultContent":'', "width":10},
907 {"data":"name", "orderable":true,
908 "render": ( data, type, row )=>{
909 return encodeURIComponent(data);
910 }
911 },
912 {"data":"time", "orderable":true, "width":80,
913 "render":function (data, type, row) {
914 return '<span style="display:none;">'+data+'</span>'+DateFormatStringToDateTimeText(data);
915 }
916 },
917 {"data":"areacode", "orderable":true, "className":"dt-center", "width":80},
918 {"data":"aktiv", "orderable":true, "width":50, "className":"dt-center", "render":(data, type, row)=>{
919 return data == 1 ? 'active' : 'inactive';
920 }},
921 {"data":null,"orderable":false,"defaultContent":'',"className":"buttons dt-right","width":100,
922 "render": ( data, type, row )=>{
923 return '<div class="et-btn-group"><button class="et-btn-action" data-type="edit">'+_x('Edit', 'label', 'event-tickets-with-ticket-scanner')+'</button></div><div class="et-btn-group et-btn-group--danger"><button class="et-btn-action et-btn-action--danger" data-type="delete">'+_x('Delete', 'label', 'event-tickets-with-ticket-scanner')+'</button></div>';
924 }
925 }
926 ]
927 });
928 tabelle.css("width", "100%");
929 table.on('click', 'button[data-type="edit"]', e=>{
930 let data = tabelle_authtokens_datatable.row( $(e.target).parents('tr') ).data();
931 __showMaskAuthtoken(data);
932 });
933 table.on('click', 'button[data-type="delete"]', e=>{
934 let data = tabelle_authtokens_datatable.row( $(e.target).parents('tr') ).data();
935 LAYOUT.renderYesNo(_x('Do you want to delete?', 'title', 'event-tickets-with-ticket-scanner'), __('Are you sure, you want to delete this auth token?', 'event-tickets-with-ticket-scanner')+'<br><p><b>'+data.name+'</b></p>'+__('The user with this auth token will not be able to use the server anymore. The user will need to add a new auth token from you.<p>The effect will be immediately.</p>', 'event-tickets-with-ticket-scanner'), ()=>{
936 let _data = {'id':data.id};
937 div_tabelle.html(_getSpinnerHTML());
938 _makePost('removeAuthtoken', _data, result=>{
939 DATA_AUTHTOKENS = null;
940 __renderTabelleAuthtokens();
941 //tabelle_authtokens_datatable.ajax.reload();
942 });
943 });
944 });
945 $('#'+table_id+' tbody').on('click', 'td.details-control', e=>{
946 function ___format(d) {
947 let metaObj = {};
948 if (d.metaObj) metaObj = d.metaObj;
949 if (d.meta && !d.metaObj) {
950 metaObj = JSON.parse(d.meta);
951 }
952 let id = 'qrcode_'+d.id+'_'+time();
953 let content = JSON.stringify({"type":"auth", "time":d.time, "name":d.name, "code":d.code, "areacode":d.areacode, "url":OPTIONS.infos.site.site_url});
954 let content2 = _getTicketScannerURL()+'&auth='+encodeURIComponent(content);
955
956 let div = $('<div/>');
957 $('<div>').html("<b>Authcode: </b>"+d.code).appendTo(div);
958 let div_wrapper = $('<div style="padding-top:10px;">').appendTo(div);
959
960 $('<div style="width:256px;float:left;text-align:center">').html('<b>Only Auth Token</b><div id="'+id+'" style="text-align:center;"></div><script>jQuery("#'+id+'").qrcode(\''+content+'\');</script>').appendTo(div_wrapper);
961 $('<div style="margin-left:20px;width:256px;float:left;text-align:center">').html('<b>With Ticket Scanner URL</b><div id="'+id+'2" style="text-align:center;"></div><script>jQuery("#'+id+'2").qrcode(\''+content2+'\');</script>').appendTo(div_wrapper);
962
963 let div_inner = $('<div style="float:left;padding-left:10px;">').appendTo(div_wrapper);
964 let _desc = metaObj.desc == "" ? "-" : metaObj.desc;
965 $('<div>').html('<b><a href="'+content2+'" target="_blank">Open Ticket Scanner with Auth Token</a></b>').appendTo(div_inner);
966 $('<div>').html('<b>Desc:</b> ').append($('<span>').text(_desc)).appendTo(div_inner);
967
968 let bound_to_products = metaObj.ticketscanner.bound_to_products == "" ? [] : metaObj.ticketscanner.bound_to_products.toString().split(",");
969 $("<div>").html("<b>Bound to product:</b> "+(bound_to_products.length == 0 ? "all products": bound_to_products.join(", "))).appendTo(div_inner);
970
971 return div;
972 }
973
974 var tr = $(e.target).parents('tr');
975 var row = tabelle_authtokens_datatable.row( tr );
976 if ( row.child.isShown() ) {
977 // This row is already open - close it
978 row.child.hide();
979 tr.removeClass('shown');
980 } else {
981 // Open this row
982 row.child( ___format(row.data()) ).show();
983 tr.addClass('shown');
984 }
985
986 });
987 });
988 }
989 __renderTabelleAuthtokens();
990 });
991 }
992
993 function _displayFAQArea() {
994 STATE = 'faq';
995 DIV.html(_getSpinnerHTML());
996
997 let questions = [
998 {
999 cat: "Getting Started",
1000 items: [
1001 {
1002 q: "How do I create my first ticket product?",
1003 t: '<p>In WooCommerce, create or edit a product. In the <b>Product data</b> panel you will find the <b>Event Ticket</b> tab. Check <b>This is a ticket</b> and assign a ticket list. Save the product. When a customer purchases this product and the order reaches the status "completed", a ticket with a unique QR code is generated automatically.</p><p>You can also check out <a href="https://youtu.be/yJcHMV7oAFc" target="_blank">this setup video</a> for a walkthrough.</p>'
1004 },
1005 {
1006 q: "What is the difference between free and premium?",
1007 t: '<p>The free version supports up to 32 tickets per product and includes the full ticket scanner, QR codes, PDF tickets, and WooCommerce integration.</p><p>The <b>premium version</b> removes the ticket limit and adds features like the visual ticket template designer (TWIG), ticket badges, seating plans, custom fields per ticket, and more.</p>'
1008 }
1009 ]
1010 },
1011 {
1012 cat: "Ticket Scanner",
1013 items: [
1014 {
1015 q: "How do I share scanner access with my event staff?",
1016 t: '<p>Go to the plugin admin and click <b>Auth Tokens</b>. Create a new token and optionally restrict it to specific products. Your staff can scan the auth token QR code with the ticket scanner on their phone &mdash; it grants scanner access without needing a WordPress login.</p>'
1017 },
1018 {
1019 q: "How do I install the scanner as an app on my phone?",
1020 t: '<p>The ticket scanner supports <b>PWA</b> (Progressive Web App). Enable the option <b>ticketScannerPWA</b> in the plugin settings. Then open the scanner URL in Chrome or Safari and use "Add to Home Screen". It will behave like a native app with its own icon and fullscreen mode.</p>'
1021 },
1022 {
1023 q: "What are scanner presets and which should I use?",
1024 t: '<p>Scanner presets are quick-access buttons at the top of the scanner that configure multiple options at once. The most popular presets:</p><ul><li><b>Fast Mode</b> &mdash; enables <b>ticketScannerScanAndRedeemImmediately</b> and <b>ticketScannerStartCamWithoutButtonClicked</b> so scanning and redeeming happens in one step without extra taps.</li><li><b>Info Mode</b> &mdash; shows full ticket details before redeeming, useful for checking names or seat numbers.</li></ul><p>You can also toggle <b>ticketScannerHideTicketInformation</b> and <b>ticketScannerVibrate</b> (haptic feedback) individually.</p>'
1025 },
1026 {
1027 q: "Can I use a hardware barcode scanner?",
1028 t: '<p>Yes. The ticket scanner page has a text input field. Any USB or Bluetooth barcode/QR scanner that acts as a keyboard input device will work. Simply focus the input field and scan &mdash; the code is entered and submitted automatically.</p>'
1029 }
1030 ]
1031 },
1032 {
1033 cat: "Tickets & PDF",
1034 items: [
1035 {
1036 q: "How do I customize the ticket PDF design?",
1037 t: '<p>The plugin uses <b>TWIG templates</b> for PDF rendering. In the plugin options, you can find the <b>Ticket Template Designer</b> section. Use the test designer to preview changes live. You have access to variables like <code>TICKET</code>, <code>ORDER</code>, <code>PRODUCT</code>, and <code>METAOBJ</code>.</p><p>Options like <b>wcTicketSizeWidth</b>, <b>wcTicketSizeHeight</b>, <b>wcTicketPDFBackgroundColor</b>, and <b>wcTicketPDFFullBleed</b> let you control the page layout. The premium version includes the visual template designer.</p>'
1038 },
1039 {
1040 q: "How do I let customers download all tickets as one PDF?",
1041 t: '<p>Enable one or more of these options to show a "Download all tickets" button:</p><ul><li><b>wcTicketDisplayDownloadAllTicketsPDFButtonOnMail</b> &mdash; link in the order confirmation email</li><li><b>wcTicketDisplayDownloadAllTicketsPDFButtonOnCheckout</b> &mdash; on the checkout thank-you page</li><li><b>wcTicketDisplayDownloadAllTicketsPDFButtonOnOrderdetail</b> &mdash; on the "My Account" order detail page</li></ul><p>The customer can then download a single PDF containing all tickets from the order.</p>'
1042 }
1043 ]
1044 },
1045 {
1046 cat: "WooCommerce",
1047 items: [
1048 {
1049 q: "How do I let customers choose their event date?",
1050 t: '<p>Edit your ticket product and enable the <b>Day Chooser</b> checkbox in the Event Ticket tab. Configure the start/end date range and optionally exclude specific weekdays. The customer will see a date picker on the product page and must choose a date before adding to cart.</p><p>Use <b>wcTicketLabelCartForDaychooser</b> to customize the label shown in the cart.</p>'
1051 },
1052 {
1053 q: "How do I handle refunds &mdash; what happens to the ticket?",
1054 t: '<p>Two options control this:</p><ul><li><b>wcRestrictFreeCodeByOrderRefund</b> &mdash; clears the ticket when the entire order is refunded or deleted</li><li><b>wcassignmentOrderItemRefund</b> &mdash; clears the ticket when an individual line item is refunded</li></ul><p>When a ticket is "cleared", it becomes invalid and cannot be scanned anymore. The ticket code is released back to the pool.</p>'
1055 },
1056 {
1057 q: "Can I auto-complete orders that only contain tickets?",
1058 t: '<p>Yes. Enable the option <b>wcTicketSetOrderToCompleteIfAllOrderItemsAreTickets</b>. When all items in the order are ticket products and the order reaches "processing" status (payment received), it is automatically set to "completed". This triggers ticket generation without manual intervention. Unpaid orders are not affected.</p>'
1059 },
1060 {
1061 q: "Can customers redeem their own ticket?",
1062 t: '<p>Yes. Enable the option <b>wcTicketShowRedeemBtnOnTicket</b> to show a redeem button on the ticket detail page. This is useful for self-check-in scenarios. You can also set <b>wcTicketRedirectUser</b> and <b>wcTicketRedirectUserURL</b> to redirect the customer to a specific page after redemption.</p>'
1063 }
1064 ]
1065 },
1066 {
1067 cat: "Validation & Security",
1068 items: [
1069 {
1070 q: "How do I prevent tickets from being scanned too early or too late?",
1071 t: '<p>Set event start and end dates on your ticket product. Then configure these options:</p><ul><li><b>wcTicketDontAllowRedeemTicketBeforeStart</b> &mdash; blocks scanning before the event starts</li><li><b>wcTicketOffsetAllowRedeemTicketBeforeStart</b> &mdash; allow scanning X hours before start (e.g. 2 hours early for entry)</li><li><b>wcTicketAllowRedeemTicketAfterEnd</b> &mdash; allow or block scanning after the event ends</li></ul><p>You can customize the error messages shown via <b>wcTicketTransTicketNotValidToEarly</b> and <b>wcTicketTransTicketNotValidToLate</b>.</p>'
1072 },
1073 {
1074 q: "How do I allow multi-use tickets (day passes, memberships)?",
1075 t: '<p>Edit your ticket product and set the <b>Max redeem amount</b> field in the Event Ticket tab. Set it to the number of times a ticket can be scanned (e.g. 30 for a monthly pass). Set it to <b>0</b> for unlimited scans.</p><p>The scanner will show how many times the ticket has been used (e.g. "Used 5 of 30").</p>'
1076 }
1077 ]
1078 },
1079 {
1080 cat: "Advanced",
1081 items: [
1082 {
1083 q: "How do I use webhooks to connect to external systems?",
1084 t: '<p>Enable <b>webhooksActiv</b> in the plugin options. Then configure URLs for the events you want to be notified about:</p><ul><li><b>webhookURLsetused</b> &mdash; ticket redeemed for the first time</li><li><b>webhookURLaddwcticketsold</b> &mdash; new ticket sold via WooCommerce</li><li><b>webhookURLaddwcticketredeemed</b> / <b>webhookURLaddwcticketunredeemed</b> &mdash; ticket redeemed or unredeemed</li></ul><p>The plugin sends a POST request with ticket data as JSON to the configured URL. You can use this to sync with CRM systems, access control, or analytics tools.</p>'
1085 },
1086 {
1087 q: "How to use own page with ticket scanner and have the QR code redirect to it?",
1088 t: "<p>You set up a page with the ticket scanner shortcode 'sasoEventTicketsValidator_ticket_scanner'.<br>Then adjust the URL for your tickets (scanner is included). The only option for now is the wcTicketCompatibilityModeURLPath. But this also changes the detail page of the ticket. Basically the system is adding to this URL just the '/scanner/?code='.</p><p>If you do not want this, you can adjust the QR content with the option <b>qrOwnQRContent</b>.</p><p>Set it to have the content:<br>https://domain-and-path/scanner/?code={WC_TICKET__PUBLIC_TICKET_ID}</p>"
1089 },
1090 {
1091 q: "How to display meta information of the purchased item?",
1092 t: 'You can display the meta information of the item with TWIG.<br>Try TWIG code in the ticket template test designer, to see if this helps. First it is a good idea to check the whole meta values. You can achieve this, by displaying the values as JSON with this code.<p><b>{% for item_id, item in ORDER.get_items %}<br>{{ item.get_meta_data|json_encode() }}<br>{% endfor %}</b></p><p>You will see the key value pairs. Then grab your values. E.g.</p><p><b>{%- for item_id, item in ORDER.get_items -%}<br>{%- if item_id == METAOBJ.woocommerce.item_id -%}<br>&lt;br&gt;Date: {{ item.get_meta("Booked From", true) }} - {{ item.get_meta("Booked To", true) }}<br>{%- endif -%}<br>{%- endfor -%}</b></p>'
1093 }
1094 ]
1095 },
1096 {
1097 cat: "Troubleshooting",
1098 items: [
1099 {
1100 q: "PDF is not rendering - critical error",
1101 t: '<p>The used PDF library cannot handle all the fancy HTML and CSS. Using these in the product description can lead to an error. If the ticket detail page is working, but the PDF not then you can try to remove the HTML tags or use the option to not print the product description to the ticket.<br>Please set the option <b>wcTicketPDFStripHTML</b> to remove the HTML and retry the PDF by reloading the browser or click again.</p><p>If your system is not live yet, you can use the debug mode first to see which HTML tags are used. The basics HTML tags are working well.</p><p>Try the option to remove the not supported HTML tags - this is not always great, because it removes the HTML tags that Wordpress is not supporting and could still lead to PDF issues, but a great start.<br>If this was not helping, then remove please the HTML tags in your product description for a test. You can also just deactivate the option <b>wcTicketDisplayShortDesc</b> to not use the short description of the product for a test.</p>'
1102 },
1103 {
1104 q: "Receiving 404 error page if calling the ticket view and/or PDF",
1105 t: '<p>Some installations have issues to open the ticket details view and/or the ticket scanner.<br>This could be because of your theme, other plugins or more stricter security settings.</p><p>If you experience to see the "file not found" page (404), then it could help if your activate the compatibility mode in the options.</p><p>For this configure the option <b>wcTicketCompatibilityModeURLPath</b> and/or <b>wcTicketCompatibilityMode</b>.</p><p>If this do not help, then the plugin will not work with your installation for now.</p>'
1106 },
1107 {
1108 q: "How to ask for a value of your ticket?",
1109 t: '<p>You can setup your product to ask your customer for up to 2 values. Free text and a value chosen from a dropdown.<br>You can checkout how it is done with <a href="https://youtu.be/2vTV39wgWNE" target="_blank">this video</a>.</p>'
1110 },
1111 {
1112 q: "(Pre)Create order with tickets in the backend",
1113 t: '<p>You can also checkout <a href="https://youtu.be/VxUV-s-SIpA" target="_blank">this video here</a>.<br>This video shows how to create an order from the backend and generate the tickets.<br>This approach is also good for free tickets. So you can create the order and have valid tickets. Do not forget to set the order to a redeemable status. The default is "completed".</p>'
1114 }
1115 ]
1116 }
1117 ];
1118
1119 let div = $('<div>');
1120 div.append("<h2>FAQ</h2>");
1121 let div2 = $('<div class="et-card">').appendTo(div);
1122 div2.append(getUseFulVideosHTML()+'<br><br>');
1123
1124 questions.forEach(cat => {
1125 div2.append($('<h2 style="margin:25px 0 10px 0;padding-bottom:5px;border-bottom:2px solid #ddd;">').text(cat.cat));
1126 cat.items.forEach(v => {
1127 let clicked = false;
1128 div2.append($('<h3 style="cursor:pointer;margin:5px 0;">').html("+ "+v.q).on("click",e=>{
1129 f1.css("display", clicked ? "none" : "block");
1130 clicked = !clicked;
1131 }));
1132 let f1 = $('<div style="display:none;padding:0 0 15px 15px;">').html(v.t).appendTo(div2);
1133 });
1134 });
1135
1136 DIV.html(getBackButtonDiv());
1137 DIV.append(div);
1138 }
1139
1140 function _displaySeatingplanArea() {
1141 STATE = 'seatingplan';
1142 let div = $('<div>').html(_getSpinnerHTML());
1143 const version = system.is_debug ? new Date().getTime() : myAjax._plugin_version;
1144 const jsFile = 'js/seating_admin.js?v=' + version;
1145 const cssFile = 'css/seating_admin';
1146
1147 addStyleTag(myAjax._plugin_home_url + '/' + cssFile + '.css?v=' + version, 'saso_seating_admin_css');
1148
1149 // Load JS if not already loaded (or always in debug mode)
1150 if (!system.is_debug && system.DYNJS[jsFile]) {
1151 sasoEventtickets_js_seating_admin(myAjax, getHelperFunktions()).initAdmin(div);
1152 } else {
1153 console.log('Loading seating admin JS: ' + jsFile);
1154 $.getScript(myAjax._plugin_home_url + '/' + jsFile, (data) => {
1155 system.DYNJS[jsFile] = data;
1156 eval(data);
1157 sasoEventtickets_js_seating_admin(myAjax, getHelperFunktions()).initAdmin(div);
1158 });
1159 }
1160
1161 return div;
1162 }
1163 function _renderLicenseStatus(container, info) {
1164 container.html('');
1165 if (!info || typeof info === 'undefined') {
1166 container.append('<div class="et-kv-row"><span class="et-kv-label">'+__('Status', 'event-tickets-with-ticket-scanner')+'</span><span class="et-badge" style="background:#f3f4f6;color:#6b7280;">'+__('Not checked yet', 'event-tickets-with-ticket-scanner')+'</span></div>');
1167 return;
1168 }
1169 let statusLabel, badgeClass;
1170 if (typeof info.active !== 'undefined') {
1171 statusLabel = info.active ? __('Active', 'event-tickets-with-ticket-scanner') : __('Inactive', 'event-tickets-with-ticket-scanner');
1172 badgeClass = info.active ? 'et-badge-success' : 'et-badge-danger';
1173 } else if (info.subscription_type === 'lifetime' || info.timestamp == -1) {
1174 statusLabel = __('Active', 'event-tickets-with-ticket-scanner') + ' (Lifetime)';
1175 badgeClass = 'et-badge-success';
1176 } else if (info.timestamp > 0 && info.timestamp * 1000 > Date.now()) {
1177 statusLabel = __('Active', 'event-tickets-with-ticket-scanner');
1178 badgeClass = 'et-badge-success';
1179 } else if (info.timestamp > 0) {
1180 statusLabel = __('Expired', 'event-tickets-with-ticket-scanner');
1181 badgeClass = 'et-badge-danger';
1182 } else {
1183 statusLabel = __('Not checked yet', 'event-tickets-with-ticket-scanner');
1184 badgeClass = '';
1185 }
1186 container.append('<div class="et-kv-row"><span class="et-kv-label">'+__('Status', 'event-tickets-with-ticket-scanner')+'</span><span class="et-badge '+badgeClass+'">'+statusLabel+'</span></div>');
1187 if (info.last_success && parseInt(info.last_success) > 0) {
1188 let lastDate = new Date(parseInt(info.last_success) * 1000);
1189 container.append('<div class="et-kv-row"><span class="et-kv-label">'+__('Last successful check', 'event-tickets-with-ticket-scanner')+'</span><span class="et-kv-value">'+lastDate.toLocaleString()+'</span></div>');
1190 }
1191 if (info.expiration_date && info.expiration_date !== '') {
1192 container.append('<div class="et-kv-row"><span class="et-kv-label">'+__('Expiration date', 'event-tickets-with-ticket-scanner')+'</span><span class="et-kv-value">'+info.expiration_date+'</span></div>');
1193 }
1194 if (info.subscription_type && info.subscription_type !== '') {
1195 container.append('<div class="et-kv-row"><span class="et-kv-label">'+__('Subscription type', 'event-tickets-with-ticket-scanner')+'</span><span class="et-kv-value" style="text-transform:capitalize;">'+info.subscription_type+'</span></div>');
1196 }
1197 let failures = parseInt(info.consecutive_failures || 0);
1198 if (failures > 0) {
1199 container.append('<div class="et-kv-row"><span class="et-kv-label">'+__('Consecutive failures', 'event-tickets-with-ticket-scanner')+'</span><span class="et-badge et-badge-danger">'+failures+'</span></div>');
1200 }
1201 if (parseInt(info.notvalid || 0) > 0) {
1202 container.append('<div class="et-kv-row"><span class="et-kv-label">'+__('Server flagged as not valid', 'event-tickets-with-ticket-scanner')+'</span><span class="et-badge et-badge-danger">'+__('Yes', 'event-tickets-with-ticket-scanner')+'</span></div>');
1203 }
1204 }
1205
1206 function _displaySupportInfoArea() {
1207 STATE = 'support';
1208 DIV.html(_getSpinnerHTML());
1209 getOptionsFromServer(reply=>{
1210 let newline = '<br>';
1211 let div_stats = $('<div/>').html(_getSpinnerHTML());
1212 let statsData = null;
1213
1214 _makeGet('getSupportInfos', {}, infos=>{
1215 statsData = infos.amount;
1216 let statsRows = '';
1217 statsRows += '<div class="et-kv-row"><span class="et-kv-label">Ticket Counter</span><span class="et-kv-value">'+reply.infos.ticket.counter+'</span></div>';
1218 statsRows += '<div class="et-kv-row"><span class="et-kv-label">Codes</span><span class="et-kv-value">'+infos.amount.codes+'</span></div>';
1219 statsRows += '<div class="et-kv-row"><span class="et-kv-label">Lists</span><span class="et-kv-value">'+infos.amount.lists+'</span></div>';
1220 statsRows += '<div class="et-kv-row"><span class="et-kv-label">IPs</span><span class="et-kv-value">'+infos.amount.ips+'</span></div>';
1221 div_stats.html('<div class="et-kv-table">'+statsRows+'</div>');
1222 // Update textarea with stats
1223 if (typeof supportTextarea !== 'undefined') {
1224 supportTextarea.val(_buildSupportText());
1225 }
1226 });
1227
1228 let data = reply.options; // options values
1229 let versions = reply.versions;
1230
1231 DIV.html(getBackButtonDiv());
1232
1233 // ── Quick Links row ──
1234 let quickLinks = $('<div class="et-support-quicklinks">').appendTo(DIV);
1235 $('<a class="et-card et-support-link" href="https://vollstart.com/event-tickets-with-ticket-scanner/docs/" target="_blank"><span class="dashicons dashicons-book" style="color:var(--et-primary);"></span><div><strong>Documentation</strong><span>'+__('Visit the full plugin docs', 'event-tickets-with-ticket-scanner')+'</span></div></a>').appendTo(quickLinks);
1236 $('<a class="et-card et-support-link" href="https://chatgpt.com/g/g-6819d8f68338819193a4be7e7973cce0-event-tickets-support-gpt" target="_blank"><span class="dashicons dashicons-format-chat" style="color:var(--et-primary);"></span><div><strong>AI Support Bot</strong><span>'+__('Get instant answers from our AI', 'event-tickets-with-ticket-scanner')+'</span></div></a>').appendTo(quickLinks);
1237 $('<a class="et-card et-support-link" href="https://vollstart.com/posts/category/eventticketupdates/" target="_blank"><span class="dashicons dashicons-megaphone" style="color:var(--et-primary);"></span><div><strong>Release Notes</strong><span>'+__('See latest updates', 'event-tickets-with-ticket-scanner')+'</span></div></a>').appendTo(quickLinks);
1238 $('<div class="et-card et-support-link" style="cursor:default;"><span class="dashicons dashicons-email" style="color:var(--et-primary);"></span><div><strong>Support Email</strong><span>support@vollstart.com</span></div></div>').appendTo(quickLinks);
1239
1240 // ── Useful Videos ──
1241 DIV.append(getUseFulVideosHTML);
1242
1243 // ── Two-column layout: left = System Info, right = License + Date + Stats ──
1244 let row1 = $('<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start;">').appendTo(DIV);
1245 let row1Right = $('<div style="display:flex;flex-direction:column;gap:16px;">').appendTo(row1);
1246
1247 // ── System Info Card (left) ──
1248 let sysCard = $('<div class="et-card">').appendTo(row1);
1249 sysCard.append('<div class="et-card-header"><span class="dashicons dashicons-info-outline" style="color:var(--et-primary);margin-right:6px;"></span>'+__('Support Context Information', 'event-tickets-with-ticket-scanner')+'</div>');
1250 sysCard.append('<p style="color:var(--et-text-secondary);font-size:13px;margin-bottom:12px;">'+__('Please copy the following information, so that we can support you better and faster. Remove any critical information if needed.', 'event-tickets-with-ticket-scanner')+'</p>');
1251
1252 // ── License & Connectivity Card (right) ──
1253 if (versions.premium != "" || isPremium() || _getOptions_Versions_getByKey('isOldPremiumDetected')) {
1254 let licenseCard = $('<div class="et-card">').appendTo(row1Right);
1255 licenseCard.append('<div class="et-card-header"><span class="dashicons dashicons-admin-network" style="color:var(--et-primary);margin-right:6px;"></span>'+__('License Status', 'event-tickets-with-ticket-scanner')+'</div>');
1256 let licenseStatusDiv = $('<div>').appendTo(licenseCard);
1257 _renderLicenseStatus(licenseStatusDiv, reply.infos.premium_expiration);
1258 let licenseBtnRow = $('<div style="display:flex;gap:8px;align-items:center;margin-top:12px;">').appendTo(licenseCard);
1259 let recheckBtn = $('<button class="button button-secondary">').html(__('Check License Now', 'event-tickets-with-ticket-scanner'));
1260 recheckBtn.on('click', function() {
1261 recheckBtn.prop('disabled', true).html(__('Checking...', 'event-tickets-with-ticket-scanner'));
1262 _makePost('recheckLicense', {}, function(result) {
1263 recheckBtn.prop('disabled', false).html(__('Check License Now', 'event-tickets-with-ticket-scanner'));
1264 if (result) _renderLicenseStatus(licenseStatusDiv, result);
1265 }, function() {
1266 recheckBtn.prop('disabled', false).html(__('Check License Now', 'event-tickets-with-ticket-scanner'));
1267 });
1268 });
1269 licenseBtnRow.append(recheckBtn);
1270 let serverCheckBtn = $('<button class="button button-secondary">').html('Check License Server');
1271 let serverCheckResult = $('<span style="font-size:13px;">');
1272 licenseBtnRow.append(serverCheckBtn).append(serverCheckResult);
1273 serverCheckBtn.on('click', function() {
1274 serverCheckBtn.prop('disabled', true).text('Checking...');
1275 serverCheckResult.html('<span style="color:var(--et-text-muted);">Connecting...</span>');
1276 _makePost('checkLicenseServer', {}, function(response) {
1277 serverCheckBtn.prop('disabled', false).text('Check License Server');
1278 if (response.success && response.reachable) {
1279 serverCheckResult.html('<span class="et-badge et-badge-success">'+response.message+'</span>');
1280 } else {
1281 serverCheckResult.html('<span class="et-badge et-badge-danger">'+response.message+'</span>');
1282 }
1283 }, function() {
1284 serverCheckBtn.prop('disabled', false).text('Check License Server');
1285 serverCheckResult.html('<span class="et-badge et-badge-danger">Connection failed</span>');
1286 });
1287 });
1288 } else {
1289 // No premium — just connectivity check
1290 let connCard = $('<div class="et-card">').appendTo(row1Right);
1291 connCard.append('<div class="et-card-header"><span class="dashicons dashicons-admin-network" style="color:var(--et-primary);margin-right:6px;"></span>License Server Connectivity</div>');
1292 connCard.append('<p style="color:var(--et-text-secondary);font-size:13px;margin-bottom:12px;">Check if the license/update server is reachable.</p>');
1293 let connBtnRow = $('<div style="display:flex;gap:8px;align-items:center;">').appendTo(connCard);
1294 let connBtn = $('<button class="button button-secondary">').html('Check License Server');
1295 let connResult = $('<span style="font-size:13px;">');
1296 connBtnRow.append(connBtn).append(connResult);
1297 connBtn.on('click', function() {
1298 connBtn.prop('disabled', true).text('Checking...');
1299 connResult.html('<span style="color:var(--et-text-muted);">Connecting...</span>');
1300 _makePost('checkLicenseServer', {}, function(response) {
1301 connBtn.prop('disabled', false).text('Check License Server');
1302 if (response.success && response.reachable) {
1303 connResult.html('<span class="et-badge et-badge-success">'+response.message+'</span>');
1304 } else {
1305 connResult.html('<span class="et-badge et-badge-danger">'+response.message+'</span>');
1306 }
1307 }, function() {
1308 connBtn.prop('disabled', false).text('Check License Server');
1309 connResult.html('<span class="et-badge et-badge-danger">Connection failed</span>');
1310 });
1311 });
1312 }
1313
1314 // System info table (appended to sysCard defined above)
1315 let sysRows = '';
1316 sysRows += '<div class="et-kv-row"><span class="et-kv-label">WordPress</span><span class="et-kv-value">'+versions.wp+'</span></div>';
1317 sysRows += '<div class="et-kv-row"><span class="et-kv-label">Requires WP</span><span class="et-kv-value">'+versions.requires_wp+'</span></div>';
1318 sysRows += '<div class="et-kv-row"><span class="et-kv-label">Tested up to</span><span class="et-kv-value">'+versions.tested_up_to+'</span></div>';
1319 sysRows += '<div class="et-kv-row"><span class="et-kv-label">PHP</span><span class="et-kv-value">'+versions.php+'</span></div>';
1320 sysRows += '<div class="et-kv-row"><span class="et-kv-label">Requires PHP</span><span class="et-kv-value">'+versions.requires_php+'</span></div>';
1321 sysRows += '<div class="et-kv-row"><span class="et-kv-label">MySQL/MariaDB</span><span class="et-kv-value">'+versions.mysql+'</span></div>';
1322 sysRows += '<div class="et-kv-row"><span class="et-kv-label">Basic Plugin</span><span class="et-kv-value">'+versions.basic+'</span></div>';
1323 sysRows += '<div class="et-kv-row"><span class="et-kv-label">Basic DB</span><span class="et-kv-value">'+versions.db+'</span></div>';
1324 if (versions.first_activated_at) {
1325 sysRows += '<div class="et-kv-row"><span class="et-kv-label">First Activated</span><span class="et-kv-value">'+_formatActivationDate(versions.first_activated_at)+'</span></div>';
1326 }
1327 if (versions.premium != "") {
1328 sysRows += '<div class="et-kv-row"><span class="et-kv-label">Premium License Key</span><span class="et-kv-value" style="font-family:monospace;font-size:12px;">'+versions.premium_serial+'</span></div>';
1329 sysRows += '<div class="et-kv-row"><span class="et-kv-label">Premium Plugin</span><span class="et-kv-value">'+versions.premium+'</span></div>';
1330 sysRows += '<div class="et-kv-row"><span class="et-kv-label">Premium DB</span><span class="et-kv-value">'+versions.premium_db+'</span></div>';
1331 }
1332 sysCard.append('<div class="et-kv-table">'+sysRows+'</div>');
1333
1334 // ── Copy-able Support Text ──
1335 // Build plain-text version of ALL support info for easy copy-paste
1336 function _buildSupportText() {
1337 let lines = [];
1338 lines.push('=== Support Context Information ===');
1339 lines.push('WordPress Version: '+versions.wp);
1340 lines.push('Requires WP: '+versions.requires_wp);
1341 lines.push('Tested up to: '+versions.tested_up_to);
1342 lines.push('PHP Version: '+versions.php);
1343 lines.push('Requires PHP: '+versions.requires_php);
1344 lines.push('MySQL/MariaDB Version: '+versions.mysql);
1345 lines.push('Product: Event Tickets with WooCommerce');
1346 lines.push('Basic Plugin Version: '+versions.basic);
1347 lines.push('Basic DB Version: '+versions.db);
1348 if (versions.first_activated_at) {
1349 lines.push('First Activated: '+_formatActivationDate(versions.first_activated_at));
1350 }
1351 if (versions.premium != "") {
1352 lines.push('Premium License Key: '+versions.premium_serial);
1353 lines.push('Premium Plugin Version: '+versions.premium);
1354 lines.push('Premium DB Version: '+versions.premium_db);
1355 }
1356 lines.push('');
1357 lines.push('=== Date & Timezone ===');
1358 lines.push('Default Timezone: '+versions.date_default_timezone);
1359 lines.push('WP Timezone: '+versions.date_WP_timezone);
1360 lines.push('WP Timezone Full: '+versions.date_WP_timezone_time);
1361 lines.push('Your Date: '+versions.date_default_timezone_time);
1362 lines.push('UTC Date: '+versions.date_UTC_timezone_time);
1363 lines.push('');
1364 lines.push('=== Stats ===');
1365 lines.push('Ticket Counter: '+reply.infos.ticket.counter);
1366 if (statsData) {
1367 lines.push('Codes: '+statsData.codes);
1368 lines.push('Lists: '+statsData.lists);
1369 lines.push('IPs: '+statsData.ips);
1370 }
1371 lines.push('');
1372 lines.push('=== URLs ===');
1373 lines.push('Multisite: '+reply.infos.site.is_multisite);
1374 lines.push('Home: '+reply.infos.site.home);
1375 lines.push('Network Home: '+reply.infos.site.network_home);
1376 lines.push('Site URL: '+reply.infos.site.site_url);
1377 lines.push('');
1378 lines.push('=== Ticket URLs ===');
1379 var _ownPath = (_getOptions_getValByKey("wcTicketCompatibilityModeURLPath")||'').replace(/^\/+|\/+$/g,'');
1380 lines.push('Detail Own URL: '+(_ownPath ? reply.infos.site.home+'/'+_ownPath : '(not set — "Ticket detail URL path" option is empty, compatibility mode off)'));
1381 lines.push('Scanner Own URL: '+(_ownPath ? reply.infos.site.home+'/'+_ownPath+'/scanner/' : '(not set — "Ticket detail URL path" option is empty, compatibility mode off)'));
1382 lines.push('Detail Default URL: '+reply.infos.ticket.ticket_base_url);
1383 lines.push('Scanner Default: '+reply.infos.ticket.ticket_scanner_path);
1384 lines.push('Detail Plugin Path: '+reply.infos.ticket.ticket_detail_path);
1385 lines.push('Scanner Plugin Path: '+reply.infos.ticket.ticket_detail_path+'scanner/');
1386 return lines.join('\n');
1387 }
1388 let supportText = _buildSupportText();
1389 let copyArea = $('<div style="margin-top:16px;">').appendTo(sysCard);
1390 let copyBtnRow = $('<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">').appendTo(copyArea);
1391 let copyBtn = $('<button class="button button-secondary">').html('<span class="dashicons dashicons-clipboard" style="vertical-align:middle;margin-right:4px;font-size:16px;"></span>'+__('Copy to clipboard', 'event-tickets-with-ticket-scanner'));
1392 let copyStatus = $('<span style="font-size:13px;">');
1393 copyBtnRow.append(copyBtn).append(copyStatus);
1394 copyBtn.on('click', function() {
1395 supportTextarea[0].select();
1396 if (navigator.clipboard) {
1397 navigator.clipboard.writeText(supportTextarea.val()).then(()=>{
1398 copyStatus.html('<span class="et-badge et-badge-success">Copied!</span>');
1399 setTimeout(()=>{ copyStatus.html(''); }, 2000);
1400 });
1401 } else {
1402 document.execCommand('copy');
1403 copyStatus.html('<span class="et-badge et-badge-success">Copied!</span>');
1404 setTimeout(()=>{ copyStatus.html(''); }, 2000);
1405 }
1406 });
1407 let supportTextarea = $('<textarea readonly class="et-support-textarea">').val(supportText).appendTo(copyArea);
1408
1409 // ── Date & Timezone Card ──
1410 let dateCard = $('<div class="et-card">').appendTo(row1Right);
1411 dateCard.append('<div class="et-card-header"><span class="dashicons dashicons-clock" style="color:var(--et-primary);margin-right:6px;"></span>Date &amp; Timezone</div>');
1412 let dateRows = '';
1413 dateRows += '<div class="et-kv-row"><span class="et-kv-label">Default Timezone</span><span class="et-kv-value">'+versions.date_default_timezone+'</span></div>';
1414 dateRows += '<div class="et-kv-row"><span class="et-kv-label">WP Timezone</span><span class="et-kv-value">'+versions.date_WP_timezone+'</span></div>';
1415 dateRows += '<div class="et-kv-row"><span class="et-kv-label">WP Timezone Full</span><span class="et-kv-value">'+versions.date_WP_timezone_time+'</span></div>';
1416 dateRows += '<div class="et-kv-row"><span class="et-kv-label">Your Date</span><span class="et-kv-value">'+versions.date_default_timezone_time+'</span></div>';
1417 dateRows += '<div class="et-kv-row"><span class="et-kv-label">UTC Date</span><span class="et-kv-value">'+versions.date_UTC_timezone_time+'</span></div>';
1418 dateCard.append('<div class="et-kv-table">'+dateRows+'</div>');
1419
1420 // ── Stats Card ──
1421 let statsCard = $('<div class="et-card">').appendTo(row1Right);
1422 statsCard.append('<div class="et-card-header"><span class="dashicons dashicons-chart-bar" style="color:var(--et-primary);margin-right:6px;"></span>Stats</div>');
1423 statsCard.append(div_stats);
1424
1425 // ── Two-column row: URLs + Libraries ──
1426 let row3 = $('<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start;">').appendTo(DIV);
1427
1428 // ── URLs Card ──
1429 let urlsCard = $('<div class="et-card">').appendTo(row3);
1430 urlsCard.append('<div class="et-card-header"><span class="dashicons dashicons-admin-links" style="color:var(--et-primary);margin-right:6px;"></span>URLs</div>');
1431 let urlRows = '';
1432 urlRows += '<div class="et-kv-row"><span class="et-kv-label">Multisite</span><span class="et-kv-value">'+reply.infos.site.is_multisite+'</span></div>';
1433 urlRows += '<div class="et-kv-row"><span class="et-kv-label">Home</span><span class="et-kv-value" style="word-break:break-all;">'+reply.infos.site.home+'</span></div>';
1434 urlRows += '<div class="et-kv-row"><span class="et-kv-label">Network Home</span><span class="et-kv-value" style="word-break:break-all;">'+reply.infos.site.network_home+'</span></div>';
1435 urlRows += '<div class="et-kv-row"><span class="et-kv-label">Site URL</span><span class="et-kv-value" style="word-break:break-all;">'+reply.infos.site.site_url+'</span></div>';
1436 urlsCard.append('<div class="et-kv-table">'+urlRows+'</div>');
1437
1438 // Ticket URLs sub-section
1439 urlsCard.append('<div class="et-card-header" style="margin-top:16px;"><span class="dashicons dashicons-tickets-alt" style="color:var(--et-primary);margin-right:6px;"></span>Ticket URLs</div>');
1440 let ticketUrlRows = '';
1441 let _ownPath = (_getOptions_getValByKey("wcTicketCompatibilityModeURLPath")||'').replace(/^\/+|\/+$/g,'');
1442 ticketUrlRows += '<div class="et-kv-row"><span class="et-kv-label">Detail Own URL</span><span class="et-kv-value" style="word-break:break-all;">'+(_ownPath ? reply.infos.site.home+'/'+_ownPath : '<i>'+__('(not set — "Ticket detail URL path" option is empty, compatibility mode off)', 'event-tickets-with-ticket-scanner')+'</i>')+'</span></div>';
1443 ticketUrlRows += '<div class="et-kv-row"><span class="et-kv-label">Scanner Own URL</span><span class="et-kv-value" style="word-break:break-all;">'+(_ownPath ? reply.infos.site.home+'/'+_ownPath+'/scanner/' : '<i>'+__('(not set — "Ticket detail URL path" option is empty, compatibility mode off)', 'event-tickets-with-ticket-scanner')+'</i>')+'</span></div>';
1444 ticketUrlRows += '<div class="et-kv-row"><span class="et-kv-label">Detail Default URL</span><span class="et-kv-value" style="word-break:break-all;">'+reply.infos.ticket.ticket_base_url+'</span></div>';
1445 ticketUrlRows += '<div class="et-kv-row"><span class="et-kv-label">Scanner Default</span><span class="et-kv-value" style="word-break:break-all;">'+reply.infos.ticket.ticket_scanner_path+'</span></div>';
1446 ticketUrlRows += '<div class="et-kv-row"><span class="et-kv-label">Detail Plugin Path</span><span class="et-kv-value" style="word-break:break-all;">'+reply.infos.ticket.ticket_detail_path+'</span></div>';
1447 ticketUrlRows += '<div class="et-kv-row"><span class="et-kv-label">Scanner Plugin Path</span><span class="et-kv-value" style="word-break:break-all;">'+reply.infos.ticket.ticket_detail_path+'scanner/</span></div>';
1448 if (reply.infos.congress) {
1449 ticketUrlRows += '<div class="et-kv-row"><span class="et-kv-label">Congress URL</span><span class="et-kv-value" style="word-break:break-all;">'+reply.infos.congress.url_pattern+(reply.infos.congress.active ? '' : ' <i>('+__('congress page disabled in options', 'event-tickets-with-ticket-scanner')+')</i>')+'</span></div>';
1450 }
1451 urlsCard.append('<div class="et-kv-table">'+ticketUrlRows+'</div>');
1452
1453 // ── Error Logs Card ──
1454 let errorCard = $('<div class="et-card">').appendTo(DIV);
1455 errorCard.append('<div class="et-card-header"><span class="dashicons dashicons-warning" style="color:var(--et-danger);margin-right:6px;"></span>Error Logs</div>');
1456 let tabelle_errorlogs_datatable;
1457 let _errToolbar = $('<div class="et-toolbar">').css('margin-bottom','12px');
1458 let _errGrpUtil = $('<div class="et-btn-group">').appendTo(_errToolbar);
1459 $('<button>').html(__('Refresh table', 'event-tickets-with-ticket-scanner')).addClass("button-secondary").on("click", ()=>{
1460 tabelle_errorlogs_datatable.ajax.reload();
1461 }).appendTo(_errGrpUtil);
1462 $('<button>').html('<span class="dashicons dashicons-download" style="vertical-align:middle;margin-right:2px;font-size:16px;"></span>'+__('Export CSV', 'event-tickets-with-ticket-scanner')).addClass("button-secondary").on("click", ()=>{
1463 window.open(_requestURL('downloadErrorLogsCSV'), '_blank');
1464 }).appendTo(_errGrpUtil);
1465 let _errGrpDanger = $('<div class="et-btn-group et-btn-group--danger">').appendTo(_errToolbar);
1466 $('<button>').html(__('Empty table', 'event-tickets-with-ticket-scanner')).addClass("sngmbh_btn-delete").on("click", ()=>{
1467 LAYOUT.renderYesNo(__('Empty table', 'event-tickets-with-ticket-scanner'), sprintf(/* translators: %s: name of ticket table */__('Do you want to empty the "%s" table? All data will be lost.', 'event-tickets-with-ticket-scanner'), _x("Error Logs", 'title', 'event-tickets-with-ticket-scanner')), ()=>{
1468 LAYOUT.renderYesNo(__('Empty table - last chance', 'event-tickets-with-ticket-scanner'), sprintf(/* translators: %s: name of ticket table */__('Are you sure? You will not be able to restore the data, except you have a backup of your database. All data will be lost.', 'event-tickets-with-ticket-scanner'), _x("Error Logs", 'title', 'event-tickets-with-ticket-scanner')), ()=>{
1469 _makeGet('emptyTableErrorLogs', null, ()=>{
1470 tabelle_errorlogs_datatable.ajax.reload();
1471 });
1472 });
1473 });
1474 }).appendTo(_errGrpDanger);
1475 _errToolbar.appendTo(errorCard);
1476
1477 let div_tabelle = $('<div>').appendTo(errorCard);
1478
1479 // ── Libraries Card ──
1480 let libCard = $('<div class="et-card">').appendTo(row3);
1481 let label_version = _x('Version', 'label', 'event-tickets-with-ticket-scanner');
1482 libCard.append('<div class="et-card-header"><span class="dashicons dashicons-admin-plugins" style="color:var(--et-primary);margin-right:6px;"></span>Used Libraries</div>');
1483 libCard.append('<ul class="et-lib-list">'
1484 +'<li><span class="et-lib-name">jQuery</span> <span class="et-badge et-badge-purple">'+jQuery.fn.jquery+'</span></li>'
1485 +'<li><span class="et-lib-name">jQuery UI</span> <span class="et-badge et-badge-purple">'+jQuery.ui.version+'</span></li>'
1486 +'<li><span class="et-lib-name">PHP TWIG</span> <span class="et-badge et-badge-purple">3.22.0</span></li>'
1487 +'<li><span class="et-lib-name">PHP QR Code</span> <span class="et-badge et-badge-purple">1.1.4</span></li>'
1488 +'<li><span class="et-lib-name">FPDI</span> <span class="et-badge et-badge-purple">2.3.7</span></li>'
1489 +'<li><span class="et-lib-name">FPDF</span> <span class="et-badge et-badge-purple">1.85</span></li>'
1490 +'<li><span class="et-lib-name">TCPDF</span> <span class="et-badge et-badge-purple">6.3.2</span></li>'
1491 +'<li><span class="et-lib-name">QR Scanner</span> <span class="et-badge et-badge-purple">1.4.2</span></li>'
1492 +'<li><span class="et-lib-name">DataTables</span> <span class="et-badge et-badge-purple">1.10.21</span></li>'
1493 +'<li><span class="et-lib-name">Raphael</span> <span class="et-badge et-badge-purple">2.3.0</span></li>'
1494 +'<li><span class="et-lib-name">Ace Editor</span></li>'
1495 +'<li><span class="et-lib-name">html5-qrcode</span> <span class="et-badge et-badge-purple">2.3.8</span></li>'
1496 +'</ul>');
1497
1498 // ── Options Dump ──
1499 let optCard = $('<div class="et-card">').appendTo(DIV);
1500 optCard.append('<div class="et-card-header"><span class="dashicons dashicons-admin-generic" style="color:var(--et-primary);margin-right:6px;"></span>Options</div>');
1501 let optionsContainer = $('<div/>').appendTo(optCard);
1502 $('<button/>').addClass("button button-secondary").html(__('Show all options', 'event-tickets-with-ticket-scanner')).appendTo(optionsContainer).on("click", function(){
1503 $(this).remove();
1504 let optTable = $('<div class="et-kv-table">');
1505 data.forEach(v=>{
1506 if (v.type != 'heading' && v.key != "serial") {
1507 if (v.additional && v.additional.doNotRender && v.additional.doNotRender === 1) {}
1508 else {
1509 let value = v.value;
1510 let def = '';
1511 if (value == '') {
1512 def = ' (DEFAULT)';
1513 value = v.default;
1514 }
1515 optTable.append('<div class="et-kv-row"><span class="et-kv-label" style="font-family:monospace;font-size:11px;">'+v.key+def+'</span><span class="et-kv-value">'+$('<span>').text(value).html()+'</span></div>');
1516 }
1517 }
1518 });
1519 optionsContainer.append(optTable);
1520 });
1521
1522 // ── Repair Tables Button ──
1523 $('<button/>').css("margin-top", "8px").addClass("sngmbh_btn-delete").html(_x("Repair tables", 'label', 'event-tickets-with-ticket-scanner')).appendTo(DIV).on("click", ()=>{
1524 LAYOUT.renderYesNo(__('Repair database tables?', 'event-tickets-with-ticket-scanner'), __('Do you really want to try to repair your database table definitions for the plugin? It should be safe, but only needed in very rare cases. You might see errors messages during the page reload - that is normal. Why not asking support, if you should do it? ;)', 'event-tickets-with-ticket-scanner'), dlg=>{
1525 dlg.html(_getSpinnerHTML());
1526 dlg.dialog({
1527 title:_x('Repaired', 'title', 'event-tickets-with-ticket-scanner'), modal:true, dialogClass: "no-close",
1528 close: function(event, ui){ abort=true; },
1529 buttons: [
1530 {
1531 text: _x('Ok', 'label', 'event-tickets-with-ticket-scanner'),
1532 click: function() {
1533 $( this ).dialog( _x('Close', 'label', 'event-tickets-with-ticket-scanner') );
1534 $( this ).html('');
1535 }
1536 }
1537 ]
1538 });
1539 _makePost('repairTables', {}, result=>{
1540 speakOutLoud(result, true);
1541 dlg.html(result);
1542 });
1543 });
1544 });
1545
1546 function __renderTabelleErrorLogs() {
1547 div_tabelle.html(_getSpinnerHTML());
1548 let table_id = myAjax.divPrefix+'_tabelle_errorlogs';
1549 let tabelle = $('<table/>').attr("id", table_id);
1550 tabelle.html('<thead><tr><th></th><th align="left">'+_x('Created', 'label', 'event-tickets-with-ticket-scanner')+'</th><th align="left">'+_x('Exception', 'label', 'event-tickets-with-ticket-scanner')+'</th><th>'+_x('Function', 'label', 'event-tickets-with-ticket-scanner')+'</th></tr></thead>');
1551 div_tabelle.html(tabelle);
1552
1553 let table = $('#'+table_id);
1554 $(table).DataTable().clear().destroy();
1555 tabelle_errorlogs_datatable = $(table).DataTable({
1556 "responsive": true,
1557 "searching": true,
1558 "ordering": true,
1559 "processing": true,
1560 "serverSide": true,
1561 "stateSave": false,
1562 "pageLength":50,
1563 "ajax": {
1564 url: _requestURL('getErrorLogs'),
1565 type: 'POST',
1566 },
1567 "order": [[ 1, "desc" ]],
1568 "columns":[
1569 {"data":null,"className":'details-control',"orderable":false,"defaultContent":'', "width":10},
1570 {"data":"time", "orderable":true, "width":80},
1571 {"data":"exception_msg", "orderable":true},
1572 {"data":"caller_name", "orderable":true},
1573 ]
1574 });
1575 tabelle.css("width", "100%");
1576 $('#'+table_id+' tbody').on('click', 'td.details-control', e=>{
1577 var tr = $(e.target).parents('tr');
1578 var row = tabelle_errorlogs_datatable.row( tr );
1579 if ( row.child.isShown() ) {
1580 // This row is already open - close it
1581 row.child.hide();
1582 tr.removeClass('shown');
1583 } else {
1584 // Open this row
1585 let d = row.data();
1586 row.child( "#"+d.id+'<br><pre>'+destroy_tags(d.msg)+'</pre>' ).show();
1587 tr.addClass('shown');
1588 }
1589
1590 });
1591 }
1592 __renderTabelleErrorLogs();
1593
1594 });
1595 }
1596
1597 /**
1598 * returns 0 if the versions are the same, 1 if version1 is greater, -1 if version2 is greater
1599 */
1600 function compareVersions(version1, version2) {
1601 const v1 = version1.split('.').map(Number);
1602 const v2 = version2.split('.').map(Number);
1603
1604 for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
1605 const num1 = v1[i] || 0;
1606 const num2 = v2[i] || 0;
1607
1608 if (num1 > num2) return 1;
1609 if (num1 < num2) return -1;
1610 }
1611
1612 /*
1613 // Example usage:
1614 const result = compareVersions('5.8.1', '5.8.2');
1615 if (result > 0) {
1616 console.log('Version 5.8.1 is greater than 5.8.2');
1617 } else if (result < 0) {
1618 console.log('Version 5.8.1 is less than 5.8.2');
1619 } else {
1620 console.log('Both versions are equal');
1621 }
1622 */
1623
1624 return 0;
1625 }
1626
1627 function _displayOptionsArea() {
1628 STATE = 'options';
1629 DIV.html(_getSpinnerHTML());
1630 getOptionsFromServer(reply=>{
1631 let data = reply.options; // options values
1632 let meta_tags_keys = reply.meta_tags_keys;
1633
1634 DIV.html(getBackButtonDiv());
1635
1636 // Create tabs
1637 let tabs = $('<div class="tabs"/>');
1638 let tabOptions = $('<div id="tab-options" class="tab-content"/>');
1639
1640 // Create tab navigation
1641 let tabNav = $('<ul class="tab-nav"/>');
1642 tabNav.append('<li><a href="#tab-options">Options</a></li>');
1643 if (isPremium() && typeof PREMIUM.displayOptionsArea_Templates !== "undefined") {
1644 tabNav.append(PREMIUM.displayOptionsArea_Tab);
1645 }
1646
1647 tabs.append(tabNav);
1648 tabs.append(tabOptions);
1649 if (isPremium() && typeof PREMIUM.displayOptionsArea_Templates !== "undefined") {
1650 tabs.append(PREMIUM.displayOptionsArea_Templates(_getOptions_Versions_getByKey('premium')));
1651 }
1652
1653 // seating plan tab
1654 let tabNavSeatingplan = $('<li><a href="#tab-seatingplan">Seating Plans</a></li>');
1655 tabNavSeatingplan.on("click", ()=>{
1656 tabSeatingplan.html(_displaySeatingplanArea());
1657 });
1658 tabNav.append(tabNavSeatingplan);
1659 let tabSeatingplan = $('<div id="tab-seatingplan" class="tab-content"/>');
1660 tabs.append(tabSeatingplan);
1661
1662 // change history tab
1663 let tabNavHistory = $('<li><a href="#tab-options-history">' + _x('Change History', 'label', 'event-tickets-with-ticket-scanner') + '</a></li>');
1664 let tabHistory = $('<div id="tab-options-history" class="tab-content"/>');
1665 let historyRendered = false;
1666 tabNavHistory.on("click", ()=>{
1667 if (!historyRendered) {
1668 historyRendered = true;
1669 __renderTabelleOptionsHistory(tabHistory);
1670 }
1671 });
1672 tabNav.append(tabNavHistory);
1673 tabs.append(tabHistory);
1674
1675 DIV.append(tabs);
1676
1677 // Populate Options tab
1678 let div_options = $('<div/>');
1679 let div_infos = $('<div style="padding-top: 50px;"/>');
1680 let resetOption_div = $('<div class="reset_option_wrap" style="padding-top: 20px;"/>');
1681 tabOptions.append(div_options);
1682 tabOptions.append('<hr>');
1683 tabOptions.append(resetOption_div);
1684 $('<button class="button reset_btn_actn">').html(_x('Reset All Options', 'label', 'event-tickets-with-ticket-scanner'))
1685 .on('click', ()=>{
1686 LAYOUT.renderYesNo(_x('Reset All Options', 'title', 'event-tickets-with-ticket-scanner'), __('Do you really want to reset all the option?', 'event-tickets-with-ticket-scanner'), ()=>{
1687 _makePost('resetOptions','', function(result) {
1688 if(result){
1689 _displayOptionsArea();
1690 }
1691 });
1692 });
1693 }).appendTo(resetOption_div);
1694 tabOptions.append(div_infos);
1695 div_infos.append('<a name="replacementtags"></a><h3>'+_x('Replacement Tags', 'title', 'event-tickets-with-ticket-scanner')+'</h3>').append('<p>'+__('You can use these replacement tags in your text messages and URLs for the meta ticket values', 'event-tickets-with-ticket-scanner')+'</p>');
1696 meta_tags_keys.forEach(v=>{
1697 let t = '<p><b>{'+v.key+'}</b>: '+v.label+'</p>';
1698 div_infos.append(t);
1699 });
1700
1701 //div_options.append('<h3>'+_x('Options', 'title', 'event-tickets-with-ticket-scanner')+'</h3>');
1702 div_options.append('<p><span class="dashicons dashicons-external"></span><a href="https://vollstart.com/event-tickets-with-ticket-scanner/docs/" target="_blank">Click here, to visit the documentation.</a></p>');
1703 div_options.append(getUseFulVideosHTML());
1704
1705 let exportImport_div = $('<div class="et-toolbar">').css({paddingTop:'10px',paddingBottom:'10px'}).appendTo(div_options);
1706 let importFileInput = $('<input type="file" accept=".json" style="display:none;">');
1707 exportImport_div.append(importFileInput);
1708 let exportImport_grp = $('<div class="et-btn-group">').appendTo(exportImport_div);
1709 $('<button class="button">').html(_x('Export Options', 'label', 'event-tickets-with-ticket-scanner'))
1710 .on('click', ()=>{
1711 _makePost('exportOptions', '', function(result) {
1712 if (result) {
1713 let json = JSON.stringify(result, null, 2);
1714 let blob = new Blob([json], {type: 'application/json'});
1715 let url = URL.createObjectURL(blob);
1716 let a = document.createElement('a');
1717 a.href = url;
1718 let date = new Date().toISOString().slice(0, 10);
1719 a.download = 'event-tickets-options-' + date + '.json';
1720 document.body.appendChild(a);
1721 a.click();
1722 document.body.removeChild(a);
1723 URL.revokeObjectURL(url);
1724 }
1725 });
1726 }).appendTo(exportImport_grp);
1727 $('<button class="button">').html(_x('Import Options', 'label', 'event-tickets-with-ticket-scanner'))
1728 .on('click', ()=>{
1729 importFileInput.val('');
1730 importFileInput.trigger('click');
1731 }).appendTo(exportImport_grp);
1732 $('<button class="button">').html('<span class="dashicons dashicons-welcome-learn-more" style="vertical-align:middle;margin-right:2px;"></span>' + _x('Start Wizard', 'label', 'event-tickets-with-ticket-scanner'))
1733 .on('click', ()=>{ __showSetupWizard(true); })
1734 .appendTo(exportImport_grp);
1735 if (isPremium()) {
1736 $('<button class="button">').html('<span class="dashicons dashicons-star-filled" style="vertical-align:middle;margin-right:2px;"></span>' + _x('Premium Wizard', 'label', 'event-tickets-with-ticket-scanner'))
1737 .on('click', ()=>{ __showPremiumWizard(true); })
1738 .appendTo(exportImport_grp);
1739 }
1740 importFileInput.on('change', function() {
1741 let file = this.files[0];
1742 if (!file) return;
1743 let reader = new FileReader();
1744 reader.onload = function(e) {
1745 let parsed;
1746 try {
1747 parsed = JSON.parse(e.target.result);
1748 } catch(err) {
1749 LAYOUT.renderFatalError(__('Invalid JSON file.', 'event-tickets-with-ticket-scanner'));
1750 return;
1751 }
1752 if (!parsed.options || typeof parsed.options !== 'object') {
1753 LAYOUT.renderFatalError(__('Invalid options file: missing options data.', 'event-tickets-with-ticket-scanner'));
1754 return;
1755 }
1756 let count = Object.keys(parsed.options).length;
1757 LAYOUT.renderYesNo(
1758 _x('Import Options', 'title', 'event-tickets-with-ticket-scanner'),
1759 __('Import %d options? This will overwrite current settings.', 'event-tickets-with-ticket-scanner').replace('%d', count),
1760 ()=>{
1761 _makePost('importOptions', {options: JSON.stringify(parsed.options)}, function(result) {
1762 if (result) {
1763 _displayOptionsArea();
1764 }
1765 });
1766 }
1767 );
1768 };
1769 reader.readAsText(file);
1770 });
1771
1772 let menu_band = $('<div style="padding-top:10px;padding-bottom:15px;">').appendTo(div_options);
1773 let menu_values = [];
1774 data.forEach(v=>{
1775 if (v.type === "heading") {
1776 menu_values.push(v);
1777 }
1778 });
1779 menu_values.sort((a,b)=>{
1780 if(a.label < b.label) { return -1; }
1781 if(a.label > b.label) { return 1; }
1782 return 0;
1783 });
1784 menu_values.forEach(v=>{
1785 $('<a href="#'+v.key+'" style="padding:5px;padding-left:0;margin-right:10px;">').html(v.label).appendTo(menu_band);
1786 });
1787 $('<a href="#topMenu" style="text-decoration:none;position:fixed;bottom:50px;right:10px;background-color:#b225cb;color:white;border-radius:15px;border:1 px solid blue;display:inline-block;padding:10px;">').html('<i class="dashicons dashicons-arrow-up"></i> Top').appendTo(div_options);
1788
1789 // Add jQuery for tab functionality
1790 $('.tab-nav a').on('click', function(e) {
1791 e.preventDefault();
1792 $('.tab-content').hide();
1793 $($(this).attr('href')).show();
1794 $('.tab-nav a').removeClass('active');
1795 $(this).addClass('active');
1796 });
1797
1798 // Show the first tab by default
1799 if (typeof PARAS.subdisplay !== "undefined" && PARAS.subdisplay == 'templates') {
1800 $('.tab-nav a').eq(1).click();
1801 } else {
1802 $('.tab-nav a:first').click();
1803 }
1804
1805 function __createTicketTemplateChooserBox(ticket_template, editor) {
1806 return $('<div style="width:250px;display:inline-block;margin-right:5px;text-align:center;">')
1807 .append('<img style="width:250px;" src="'+myAjax._plugin_home_url+'/img/ticket_templates/'+ticket_template.image_url+'">')
1808 .append("<br>Zero-Padding: "+(ticket_template.wcTicketPDFZeroMarginTest ? "Yes" : "No"))
1809 .append(", Size: ("+ticket_template.wcTicketSizeWidthTest+'x'+ticket_template.wcTicketSizeHeightTest+")")
1810 .append("<br>")
1811 .append($('<button class="button button-primary">').text(__('Load template','event-tickets-with-ticket-scanner')).on("click", ()=>{
1812 LAYOUT.renderYesNo(_x('Load Template Ticket Code', 'title', 'event-tickets-with-ticket-scanner'),
1813 __('Do you want to replace the test ticket template code with this template?', 'event-tickets-with-ticket-scanner')+'<br><p><img style="width:250px;" src="'+myAjax._plugin_home_url+'/img/ticket_templates/'+ticket_template.image_url+'"></p><p>Following values will be changed:'
1814 +'<br><b>wcTicketPDFZeroMarginTest</b>: '+(ticket_template.wcTicketPDFZeroMarginTest ? "Yes" : "No")
1815 +'<br><b>wcTicketPDFisRTLTest</b>: '+(ticket_template.wcTicketPDFisRTLTest ? "Yes" : "No")
1816 +'<br><b>wcTicketSizeWidthTest</b>: '+ticket_template.wcTicketSizeWidthTest
1817 +'<br><b>wcTicketSizeHeightTest</b>: '+ticket_template.wcTicketSizeHeightTest
1818 +'<br><b>wcTicketQRSizeTest</b>: '+ticket_template.wcTicketQRSizeTest
1819 +'</p>'
1820 , ()=>{
1821 editor.wcTicketDesignerTemplateTest_editor.setValue(ticket_template.wcTicketDesignerTemplateTest);
1822 $('input[data-key="wcTicketPDFZeroMarginTest"').prop("checked",ticket_template.wcTicketPDFZeroMarginTest).trigger("change");
1823 $('input[data-key="wcTicketPDFisRTLTest"').prop("checked",ticket_template.wcTicketPDFisRTLTest).trigger("change");
1824 $('input[data-key="wcTicketSizeWidthTest"').val(ticket_template.wcTicketSizeWidthTest).trigger("change");
1825 $('input[data-key="wcTicketSizeHeightTest"').val(ticket_template.wcTicketSizeHeightTest).trigger("change");
1826 $('input[data-key="wcTicketQRSizeTest"').val(ticket_template.wcTicketQRSizeTest).trigger("change");
1827 let value = editor.wcTicketDesignerTemplateTest_editor.getValue().trim();
1828 _saveOptionValue("wcTicketDesignerTemplateTest", value);
1829 editor.wcTicketDesignerTemplateTest_btn.prop("disabled", true);
1830
1831 });
1832 }));
1833 }
1834
1835 // render die input felder
1836 function __getOptionByKey(key) {
1837 for(let a=0;a<data.length;a++) {
1838 if (key == data[a].key) return data[a];
1839 }
1840 return null;
1841 }
1842
1843 let editor = {}; // for ace editor
1844 data.forEach(v=>{
1845 if (typeof v.additional !== "undefined" && v.additional.doNotRender) return;
1846 if (v.type == "heading") {
1847 let desc = v.desc;
1848 if (typeof v._doc_video !== "undefined" && v._doc_video != "") {
1849 desc += ' <span class="dashicons dashicons-external"></span> <a href="'+v._doc_video+'" target="_blank">Video Help</a>';
1850 }
1851 div_options.append('<hr>').append('<h3 id="'+v.key+'" '+(desc !== "" ? ' style="margin-bottom:0;"' : '')+'>'+v.label+'</h3>').append(desc !== "" ? '<div style="margin-bottom:15px;"><i>'+desc+'</i></div>':'');
1852 } else if (v.type =="desc") {
1853 let desc = v.desc+" ";
1854 if (typeof v._do_not_trim !== "undefined" && v._do_not_trim) {
1855 desc += 'To leave this value blank, enter a space. ';
1856 }
1857 if (typeof v._doc_video !== "undefined" && v._doc_video != "") {
1858 desc += '<span class="dashicons dashicons-external"></span> <a href="'+v._doc_video+'" target="_blank">Video Help</a>';
1859 }
1860 div_options.append('<div/>').css({"margin-bottom": "15px","margin-right": "15px"}).append('<b>'+v.label+'</b><br>'+desc+"<br>");
1861 } else {
1862 let elem_div = $('<div/>').css({"margin-bottom": "15px","margin-right": "15px"});
1863 let elem_input = $('<input type="'+v.type+'">');
1864 elem_input.attr("placeholder", v.default);
1865 if (typeof v.additional !== "undefined" && typeof v.additional.disabled !== "undefined") {
1866 elem_input.attr("disabled", true);
1867 }
1868
1869 let cbf = null;
1870 let pcbf = null;
1871 let value = v.value;
1872 if (typeof v._do_not_trim !== "undefined" && v._do_not_trim) {
1873 } else {
1874 value = (""+v.value) !== "" ? (""+v.value).trim() : ""+v.default;
1875 }
1876
1877 v.label = v.label + ' <span style="color:grey;">{'+v.key+'}</span>';
1878 if (typeof v._doc_video !== "undefined" && v._doc_video != "") {
1879 v.label += ' <span class="dashicons dashicons-external"></span> <a href="'+v._doc_video+'" target="_blank">Video Help</a>';
1880 }
1881
1882 switch (v.type) {
1883 case "editor":
1884 elem_input = $('<div id="'+v.key+'_editor" style="height:'+(typeof v.additional !== "undefined" && typeof v.additional.height !== "undefined" ? v.additional.height : '500px')+';">').text(value.trim());
1885 break;
1886 case "textarea":
1887 elem_input = $('<textarea>');
1888 elem_input.attr("placeholder", v.default);
1889 elem_input.val(value);
1890 if (typeof v.additional !== "undefined" && typeof v.additional.rows !== "undefined") {
1891 elem_input.attr("rows", v.additional.rows);
1892 }
1893 break;
1894 case "vtext":
1895 elem_input = $('<textarea>');
1896 elem_input.val(value);
1897 elem_input.attr("id", "vtext_" + v.key);
1898 elem_input.attr("data-vtext-key", v.key);
1899 if (typeof v.additional !== "undefined" && typeof v.additional.rows !== "undefined") {
1900 elem_input.attr("rows", v.additional.rows);
1901 }
1902 if (typeof v.additional !== "undefined" && typeof v.additional.tags !== "undefined") {
1903 elem_input.data("vtextTags", v.additional.tags);
1904 }
1905 break;
1906 case "checkbox":
1907 // Accept any truthy representation (1, "1", true, "true", "yes") so a
1908 // default-on checkbox renders checked even before it is first saved.
1909 elem_input.prop("checked", v.value === true || v.value === "true" || v.value === "yes" || intval(v.value) === 1);
1910 elem_input.on("change", function(){
1911 _makePost('changeOption', {'key':v.key, 'value':elem_input[0].checked ? 1:0});
1912 });
1913 elem_div.html(elem_input).append(v.label).append(v.desc !== "" ? '<br><i>'+v.desc+'</i>':'');
1914 break;
1915 case "number":
1916 if (typeof v.additional.min !== "undefined") elem_input.attr("min", v.additional.min);
1917 break;
1918 case "dropdown":
1919 elem_input = $('<select>');
1920 if (v.additional.multiple) {
1921 elem_input.prop("multiple", true);
1922 }
1923 v.additional.values.forEach(_v=>{
1924 $('<option>').attr("value", _v.value).html(_v.label).appendTo(elem_input);
1925 });
1926 if (v.additional.multiple) {
1927 if (v.value.length == 0) {
1928 value = v.default;
1929 } else {
1930 value = v.value;
1931 }
1932 } else {
1933 if (value == "") value = 1;
1934 }
1935 elem_input.val(value);
1936 break;
1937 case "media":
1938 let image_info = $('<div>');
1939 let image = $('<image style="display:none;">');
1940 let image_btn_del = $('<button class="sngmbh_btn sngmbh_btn-delete" style="display:none;">').html(_x('Remove file', 'label', 'event-tickets-with-ticket-scanner'));
1941 image_btn_del.on('click', ()=>{
1942 LAYOUT.renderYesNo(_x('Remove file', 'title', 'event-tickets-with-ticket-scanner'), __('Do you really want to remove the file information from this option?', 'event-tickets-with-ticket-scanner'), ()=>{
1943 elem_input.val("");
1944 elem_input.trigger("change");
1945 _renderMedia(0, v, image_info, image, image_btn_del);
1946 });
1947 });
1948 if (typeof v.additional == "undefined") v.additional = {};
1949 if (v.additional.max) {
1950 if (v.additional.max.width) {
1951 image.css("max-width", v.additional.max.width+'px');
1952 }
1953 if (v.additional.max.height) {
1954 image.css("max-height", v.additional.max.height+'px');
1955 }
1956 }
1957 elem_input.attr("type", "hidden");
1958 let image_btn_add = $('<button style="display:block;" />').addClass("button-primary")
1959 .html(v.additional.button)
1960 .on("click", ()=>{
1961 let is_multiple = typeof v.additional.is_multiple != "undefined" ? v.additional.is_multiple : false;
1962 let imgContainer = null;
1963 let type_filter = typeof v.additional.type_filter != "undefined" ? v.additional.type_filter : null;
1964 _openMediaChooser(elem_input, is_multiple, imgContainer, type_filter);
1965 });
1966 $('<div/>').css({"margin-bottom": "15px","margin-right": "15px"})
1967 .html(v.label+'<br>')
1968 .append(image_btn_add)
1969 .append(v.desc !== "" ? '<i>'+v.desc+'</i>':'')
1970 .append(elem_input)
1971 .append(image_info)
1972 .append(image)
1973 .append(image_btn_del)
1974 .appendTo(elem_div);
1975 _renderMedia(value, v, image_info, image, image_btn_del);
1976 pcbf = function() {
1977 image_info.html(_getSpinnerHTML());
1978 image.css('display', 'none');
1979 }
1980 cbf = function () {
1981 let value = elem_input.val();
1982 _renderMedia(value, v, image_info, image, image_btn_del);
1983 }
1984 break;
1985 }
1986
1987 if (v.type != "checkbox") {
1988 if (v.type != "media") {
1989 elem_div.html(v.label+'<br>').append(elem_input);
1990 let desc = v.desc+" ";
1991 if (typeof v._do_not_trim !== "undefined" && v._do_not_trim) {
1992 desc += 'To leave this value blank, enter a space. ';
1993 }
1994 desc = desc.trim();
1995 elem_div.append(desc !== "" ? '<br><i>'+desc+'</i>':'');
1996 }
1997 if (v.type != "number" && v.type != "color" && v.type != "vtext") {
1998 elem_input.css({"width":"90%"});
1999 }
2000 if (v.type != "dropdown" && v.type != "editor" && v.type != "vtext") {
2001 elem_input.attr("value",value);
2002 }
2003 if (v.type != "editor" && v.type != "vtext") {
2004 elem_input.on("change", ()=>{
2005 let value = elem_input.val();
2006 _saveOptionValue(v.key, value, cbf, pcbf);
2007 });
2008 }
2009 }
2010
2011 elem_input.attr("data-key", v.key);
2012
2013 if (v.key == "serial") {
2014 let serialStatusSpan = $('<span style="margin-left:10px;">');
2015 let serialCheckBtn = $('<button class="button button-secondary">').html(__('Check License', 'event-tickets-with-ticket-scanner'));
2016 serialCheckBtn.on('click', function(e) {
2017 e.preventDefault();
2018 serialCheckBtn.prop('disabled', true).html(__('Checking...', 'event-tickets-with-ticket-scanner'));
2019 serialStatusSpan.html('');
2020 _makePost('recheckLicense', {}, function(result) {
2021 serialCheckBtn.prop('disabled', false).html(__('Check License', 'event-tickets-with-ticket-scanner'));
2022 if (result) {
2023 let color = result.active ? 'green' : 'red';
2024 let label = result.active ? __('Active', 'event-tickets-with-ticket-scanner') : __('Inactive', 'event-tickets-with-ticket-scanner');
2025 if (result.subscription_type === 'lifetime') label += ' (Lifetime)';
2026 serialStatusSpan.html('<span style="color:'+color+';font-weight:bold;">'+label+'</span>');
2027 if (result.consecutive_failures > 0) {
2028 serialStatusSpan.append(''+result.consecutive_failures+' '+__('failures', 'event-tickets-with-ticket-scanner'));
2029 }
2030 }
2031 }, function() {
2032 serialCheckBtn.prop('disabled', false).html(__('Check License', 'event-tickets-with-ticket-scanner'));
2033 serialStatusSpan.html('<span style="color:red;">'+__('Error', 'event-tickets-with-ticket-scanner')+'</span>');
2034 });
2035 });
2036 elem_div.append(serialCheckBtn).append(serialStatusSpan);
2037 }
2038
2039 if (v.key == "wcassignmentUseGlobalSerialFormatter") {
2040 let option = __getOptionByKey('wcassignmentUseGlobalSerialFormatter_values');
2041 let formatterValues = null;
2042 if (option.value != "") {
2043 try {
2044 formatterValues = JSON.parse(option.value);
2045 } catch (e) {
2046 //console.log(e);
2047 }
2048 }
2049 let extra_div = $('<div>').appendTo(elem_div).css("margin-top", "10px").css("margin-left", "50px").css("padding", "10px").css("border", "1px solid black");
2050 // render here den formatter
2051 let serialCodeFormatter = _form_fields_serial_format(extra_div);
2052 serialCodeFormatter.setNoNumberOptions();
2053 serialCodeFormatter.setFormatterValues(formatterValues);
2054 serialCodeFormatter.setCallbackHandle(_formatterValues=>{
2055 // speicher formatterValues
2056 _makePost('changeOption', {'key':'wcassignmentUseGlobalSerialFormatter_values', 'value':JSON.stringify(_formatterValues)});
2057 });
2058 serialCodeFormatter.render();
2059 }
2060
2061 if (v.key == "wcTicketDesignerTemplate") {
2062 $('<button class="button button-primary">').html("Show Default Template").on("click", e=>{
2063 LAYOUT.renderInfoBox(_x('Ticket Default Template', 'title', 'event-tickets-with-ticket-scanner'), $('<textarea style="width:100%;height:400px">').val(v.default));
2064 }).appendTo(div_options);
2065 }
2066
2067 if (v.type == "editor") {
2068 //https://ace.c9.io/#nav=howto
2069 let btn_group = $('<div>').prependTo(elem_div);
2070 editor[v.key+"_editor"] = null; // will be filled later
2071 editor[v.key+"_btn"] = $('<button class="button button-primary">').prop("disabled", true).html(_x('Save Template Code', 'title', 'event-tickets-with-ticket-scanner')).on("click", evt=>{
2072 let value = editor[v.key+"_editor"].getValue().trim();
2073 _saveOptionValue(v.key, value, cbf, pcbf);
2074 editor[v.key+"_btn"].prop("disabled", true);
2075 }).appendTo(btn_group);
2076 $('<button class="sngmbh_btn-delete">').html(_x('Copy Template Code To Live Code', 'title', 'event-tickets-with-ticket-scanner')).on("click", evt=>{
2077 LAYOUT.renderYesNo(_x('Replace Live Template Code', 'title', 'event-tickets-with-ticket-scanner'), __('Do you want to replace the live template code with the template code from the test?', 'event-tickets-with-ticket-scanner'), ()=>{
2078 let value = editor[v.key+"_editor"].getValue().trim();
2079 $('input[data-key="'+v.key.replace("Test", "")+'"').val(value).trigger("change");
2080 if (v.key == "wcTicketDesignerTemplateTest") {
2081 $('input[data-key="wcTicketPDFZeroMargin"').prop("checked",$('input[data-key="wcTicketPDFZeroMarginTest"').is(':checked')).trigger("change");
2082 $('input[data-key="wcTicketPDFFullBleed"').prop("checked",$('input[data-key="wcTicketPDFFullBleedTest"').is(':checked')).trigger("change");
2083 $('input[data-key="wcTicketPDFisRTL"').prop("checked",$('input[data-key="wcTicketPDFisRTLTest"').is(':checked')).trigger("change");
2084 $('input[data-key="wcTicketSizeWidth"').val($('input[data-key="wcTicketSizeWidthTest"').val()).trigger("change");
2085 $('input[data-key="wcTicketSizeHeight"').val($('input[data-key="wcTicketSizeHeightTest"').val()).trigger("change");
2086 $('input[data-key="wcTicketQRSize"').val($('input[data-key="wcTicketQRSizeTest"').val()).trigger("change");
2087 $('input[data-key="wcTicketPDFBackgroundColor"').val($('input[data-key="wcTicketPDFBackgroundColorTest"').val()).trigger("change");
2088 }
2089 });
2090 }).appendTo(btn_group);
2091
2092 if (v.key == "wcTicketDesignerTemplateTest") {
2093 let ticket_test_chooser = $('<div>');
2094 let ticket_template_chooser = $('<div style="padding-top:5px;padding-bottom:20px;">').html('<b>Templates</b><br>You can choose from the templates below to have a starting point.<br>').appendTo(ticket_test_chooser);
2095 let ticket_test_select = $('<select>').appendTo(ticket_test_chooser);
2096 let ticket_test_direct_input = $('<input type="text" style="width:180px;" placeholder="or enter a public ticket number">');
2097 // display the template thumbnails
2098 for(let a=0;a<reply.ticket_templates.length;a++) {
2099 let ticket_template = reply.ticket_templates[a];
2100 __createTicketTemplateChooserBox(ticket_template, editor).appendTo(ticket_template_chooser);
2101 }
2102
2103 if (OPTIONS.tickets_for_testing.length > 0) {
2104 let option_values = [];
2105 for(let a=0;a<OPTIONS.tickets_for_testing.length;a++) {
2106 let ticket = OPTIONS.tickets_for_testing[a];
2107 let metaObj = null;
2108 try {
2109 metaObj = JSON.parse(ticket.meta);
2110 } catch(e) {}
2111 if (metaObj != null) {
2112 option_values.push({t:ticket, m:metaObj});
2113 }
2114 }
2115 if (option_values.length > 0) {
2116 for(let a=0;a<option_values.length;a++) {
2117 let item = option_values[a];
2118 $('<option value="'+item.m.wc_ticket._public_ticket_id+'">')
2119 .text("Order Id: "+item.t.order_id+" - "+item.m.wc_ticket._public_ticket_id+" - "+item.t._PRODUCT_NAME+" (#"+item.m.woocommerce.product_id+")")
2120 .attr("data-url-pdf", item.m.wc_ticket._url)
2121 .appendTo(ticket_test_select);
2122 }
2123 ticket_test_direct_input.appendTo(ticket_test_chooser);
2124 $('<button class="button button-primary" id="wcTicketDesignerTemplateTest_button_PDF">')
2125 .html(__('Preview Test Template Code as PDF', 'event-tickets-with-ticket-scanner')).
2126 appendTo(ticket_test_chooser).on("click", ()=>{
2127 let ticket_url = ticket_test_select.find(":selected").attr("data-url-pdf");
2128 let v = ticket_test_direct_input.val().trim();
2129 if (v != "") {
2130 ticket_url = reply.infos.ticket.ticket_base_url + v; // myAjax.ticket_base_url
2131 }
2132 iframe.attr("src", ticket_url+'?pdf&testDesigner=1&t='+time()+'&nonce='+DATA.nonce);
2133 iframe
2134 .css("width", "80%")
2135 .css("height", "500px")
2136 .css("margin-top", "10px")
2137 .css("display", "block");
2138 });
2139 let iframe = $('<iframe style="display:none;">').appendTo(ticket_test_chooser);
2140 } else {
2141 $('<option value="">').text(__("ticket cannot be used. Public Ticket Id missing.",'event-tickets-with-ticket-scanner')).appendTo(ticket_test_select);
2142 }
2143 } else {
2144 $('<option value="">').text(__("no ticket for preview available", 'event-tickets-with-ticket-scanner')).appendTo(ticket_test_select);
2145 }
2146 ticket_test_chooser.appendTo(elem_div);
2147 }
2148 }
2149
2150 elem_div.appendTo(div_options);
2151 }
2152 });
2153 __renderContextSuggestions(div_options);
2154 if (window.location.hash != "") {
2155 window.setTimeout(()=>{
2156 let h = window.location.hash;
2157 window.location.hash = "";
2158 window.location.hash = h;
2159 }, 250);
2160 }
2161 window.setTimeout(()=>{
2162 for(var k in editor) {
2163 if (k.substring(k.length -7) == "_editor") {
2164 editor[k] = ace.edit(k);
2165 //editor.wcTicketDesignerTemplateTest_editor.setTheme("ace/theme/monokai");
2166 editor[k].session.setMode("ace/mode/twig");
2167 editor[k].setShowPrintMargin(false);
2168 editor[k].commands.addCommand({name:'save', bindKey:{win:'Ctrl-S', mac:'Command-S'}, readOnly:false, exec:myEditor=>{
2169 myEditor.trigger("change");
2170 }});
2171 editor[k].session.on("change", delta=>{
2172 editor[k.replace("_editor", "_btn")].prop("disabled", false);
2173 });
2174 }
2175 }
2176 _initVtextEditors();
2177 }, 250)
2178
2179 function __renderTabelleOptionsHistory(container) {
2180 container.html('');
2181 let tabelle_history_datatable;
2182 let table_id = myAjax.divPrefix+'_tabelle_options_history';
2183
2184 $('<div class="et-toolbar">').css('margin-bottom','10px')
2185 .append($('<div class="et-btn-group">').append(
2186 $('<button>').html(__('Refresh table', 'event-tickets-with-ticket-scanner')).addClass("button-secondary").on("click", ()=>{
2187 tabelle_history_datatable.ajax.reload();
2188 })
2189 ))
2190 .appendTo(container);
2191
2192 let tabelle = $('<table/>').attr("id", table_id);
2193 tabelle.html('<thead><tr>'
2194 +'<th></th>'
2195 +'<th align="left">'+_x('Date', 'label', 'event-tickets-with-ticket-scanner')+'</th>'
2196 +'<th align="left">'+_x('Option', 'label', 'event-tickets-with-ticket-scanner')+'</th>'
2197 +'<th align="left">'+_x('Old Value', 'label', 'event-tickets-with-ticket-scanner')+'</th>'
2198 +'<th align="left">'+_x('New Value', 'label', 'event-tickets-with-ticket-scanner')+'</th>'
2199 +'<th align="left">'+_x('Changed By', 'label', 'event-tickets-with-ticket-scanner')+'</th>'
2200 +'<th></th>'
2201 +'</tr></thead>');
2202 container.append(tabelle);
2203
2204 $(tabelle).DataTable().clear().destroy();
2205 tabelle_history_datatable = $(tabelle).DataTable({
2206 "responsive": true,
2207 "searching": true,
2208 "ordering": true,
2209 "processing": true,
2210 "serverSide": true,
2211 "stateSave": false,
2212 "pageLength": 25,
2213 "ajax": {
2214 url: _requestURL('getOptionsHistory'),
2215 type: 'POST',
2216 },
2217 "order": [[ 1, "desc" ]],
2218 "columns": [
2219 {"data": null, "className": 'details-control', "orderable": false, "defaultContent": '', "width": 10},
2220 {"data": "changed_at", "orderable": true, "width": 140},
2221 {"data": "option_key", "orderable": true},
2222 {"data": "old_value", "orderable": false, "render": function(data) {
2223 if (data && data.length > 60) return '<span title="'+destroy_tags(data)+'">'+destroy_tags(data.substring(0, 60))+'&hellip;</span>';
2224 return destroy_tags(data);
2225 }},
2226 {"data": "new_value", "orderable": false, "render": function(data) {
2227 if (data && data.length > 60) return '<span title="'+destroy_tags(data)+'">'+destroy_tags(data.substring(0, 60))+'&hellip;</span>';
2228 return destroy_tags(data);
2229 }},
2230 {"data": "changed_by_name", "orderable": true, "width": 120, "defaultContent": ""},
2231 {"data": null, "orderable": false, "width": 80, "render": function(data, type, row) {
2232 return '<div class="et-btn-group"><button class="et-btn-action saso-revert-btn" data-history-id="'+row.id+'">'+_x('Revert', 'label', 'event-tickets-with-ticket-scanner')+'</button></div>';
2233 }},
2234 ]
2235 });
2236 tabelle.css("width", "100%");
2237
2238 // details-control: expand row to show full values
2239 $('#'+table_id+' tbody').on('click', 'td.details-control', e=>{
2240 var tr = $(e.target).parents('tr');
2241 var row = tabelle_history_datatable.row(tr);
2242 if (row.child.isShown()) {
2243 row.child.hide();
2244 tr.removeClass('shown');
2245 } else {
2246 let d = row.data();
2247 row.child(
2248 '<div style="padding:5px 10px;">'
2249 +'<b>'+_x('Old Value', 'label', 'event-tickets-with-ticket-scanner')+':</b><pre style="white-space:pre-wrap;max-height:200px;overflow:auto;">'+destroy_tags(d.old_value)+'</pre>'
2250 +'<b>'+_x('New Value', 'label', 'event-tickets-with-ticket-scanner')+':</b><pre style="white-space:pre-wrap;max-height:200px;overflow:auto;">'+destroy_tags(d.new_value)+'</pre>'
2251 +'</div>'
2252 ).show();
2253 tr.addClass('shown');
2254 }
2255 });
2256
2257 // revert button
2258 $('#'+table_id+' tbody').on('click', '.saso-revert-btn', e=>{
2259 let btn = $(e.target);
2260 let historyId = btn.data('history-id');
2261 let tr = btn.closest('tr');
2262 let row = tabelle_history_datatable.row(tr);
2263 let d = row.data();
2264 LAYOUT.renderYesNo(
2265 _x('Revert Option', 'title', 'event-tickets-with-ticket-scanner'),
2266 sprintf(__('Revert option "%s" to its previous value?', 'event-tickets-with-ticket-scanner'), d.option_key),
2267 ()=>{
2268 _makePost('revertOption', {history_id: historyId}, function(result) {
2269 if (result) {
2270 _displayOptionsArea();
2271 }
2272 });
2273 }
2274 );
2275 });
2276 }
2277
2278 });
2279 }
2280
2281 // ── Congresses Area ──
2282
2283 function _displayCongressesArea() {
2284 STATE = 'congresses';
2285 DIV.html('');
2286 DIV.append(getBackButtonDiv());
2287 var $container = $('<div/>').attr('id', 'saso-et-congress-app').appendTo(DIV);
2288
2289 var _congressV = myAjax._congress_assets_v || myAjax._plugin_version;
2290 if (!document.getElementById('saso-et-congress-css')) {
2291 $('<link/>', {
2292 id: 'saso-et-congress-css',
2293 rel: 'stylesheet',
2294 href: myAjax._plugin_home_url + '/css/congress-admin.css?v=' + _congressV
2295 }).appendTo('head');
2296 }
2297
2298 var cfg = { ajaxUrl: myAjax.url, nonce: myAjax.nonce, layout: LAYOUT, variables: myAjax._congressVariables || [] };
2299 if (window.sasoEtCongressAdmin) {
2300 window.sasoEtCongressAdmin.init($container, cfg);
2301 } else {
2302 $.getScript(myAjax._plugin_home_url + '/js/congress-admin.js?v=' + _congressV, function () {
2303 if (window.sasoEtCongressAdmin) {
2304 window.sasoEtCongressAdmin.init($container, cfg);
2305 }
2306 });
2307 }
2308 }
2309
2310 // ── Attendance Area with Tabs (#236) ──
2311
2312 function _displayAttendanceArea() {
2313 STATE = 'attendance';
2314 DIV.html(getBackButtonDiv());
2315
2316 // Tab bar
2317 let tabBar = $('<div style="display:flex;gap:0;margin-bottom:16px;border-bottom:2px solid #e5e7eb;">').appendTo(DIV);
2318 let tabSold = $('<button class="button" style="border:none;border-bottom:2px solid var(--et-primary);margin-bottom:-2px;border-radius:0;font-weight:600;">').html(_x('Sold Tickets', 'tab', 'event-tickets-with-ticket-scanner')).appendTo(tabBar);
2319 let tabRedemption = $('<button class="button" style="border:none;border-bottom:2px solid transparent;margin-bottom:-2px;border-radius:0;color:#666;">').html(_x('Daily Redemption Summary', 'tab', 'event-tickets-with-ticket-scanner')).appendTo(tabBar);
2320
2321 let tabContentSold = $('<div>').appendTo(DIV);
2322 let tabContentRedemption = $('<div style="display:none;">').appendTo(DIV);
2323
2324 function switchTab(active) {
2325 if (active === 'sold') {
2326 tabSold.css({'border-bottom-color':'var(--et-primary)','color':'','font-weight':'600'});
2327 tabRedemption.css({'border-bottom-color':'transparent','color':'#666','font-weight':''});
2328 tabContentSold.show();
2329 tabContentRedemption.hide();
2330 } else {
2331 tabRedemption.css({'border-bottom-color':'var(--et-primary)','color':'','font-weight':'600'});
2332 tabSold.css({'border-bottom-color':'transparent','color':'#666','font-weight':''});
2333 tabContentRedemption.show();
2334 tabContentSold.hide();
2335 }
2336 }
2337 tabSold.on('click', () => switchTab('sold'));
2338 tabRedemption.on('click', () => switchTab('redemption'));
2339
2340 // ── Tab 1: Sold Tickets Calendar ──
2341 _buildSoldTicketsTab(tabContentSold);
2342
2343 // ── Tab 2: Daily Redemption Summary ──
2344 _buildRedemptionTab(tabContentRedemption);
2345 }
2346
2347 function _buildSoldTicketsTab(container) {
2348 container.empty();
2349
2350 let now = new Date();
2351 let calMonth = now.getMonth(); // 0-based
2352 let calYear = now.getFullYear();
2353
2354 // ── Filter box ──
2355 let filterCard = $('<div class="et-card" style="padding:12px 16px;margin-bottom:16px;">').appendTo(container);
2356 let productRow = $('<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:8px;">').appendTo(filterCard);
2357 productRow.append('<strong style="margin-right:4px;">' + __('Products:', 'event-tickets-with-ticket-scanner') + '</strong>');
2358 let productCheckboxes = $('<span style="display:inline-flex;gap:12px;flex-wrap:wrap;">').appendTo(productRow);
2359
2360 // Export row
2361 let today = now.toISOString().split('T')[0];
2362 let exportRow = $('<div style="display:flex;gap:15px;align-items:flex-end;flex-wrap:wrap;">').appendTo(filterCard);
2363 let div_from = _createDivInput(_x('Export from', 'label', 'event-tickets-with-ticket-scanner')).appendTo(exportRow);
2364 let input_from = $('<input type="date"/>').val(today).css('padding','4px 8px').appendTo(div_from);
2365 let div_to = _createDivInput(_x('Export to', 'label', 'event-tickets-with-ticket-scanner')).appendTo(exportRow);
2366 let input_to = $('<input type="date"/>').val(today).css('padding','4px 8px').appendTo(div_to);
2367 $('<button/>').addClass("button-secondary").css({'margin-bottom':'15px'}).html('<span class="dashicons dashicons-download" style="vertical-align:middle;margin-right:2px;"></span>' + _x('Export CSV', 'button', 'event-tickets-with-ticket-scanner')).on('click', function() {
2368 let df = input_from.val(), dt = input_to.val();
2369 if (!df || !dt) return;
2370 window.open(_requestURL('downloadSoldTicketsCSV', {date_from: df, date_to: dt, exclude_products: getExcluded().join(',')}), '_blank');
2371 }).appendTo(exportRow);
2372
2373 // ── Calendar box ──
2374 let calCard = $('<div class="et-card" style="padding:16px;">').appendTo(container);
2375
2376 // Nav: « month year » [Refresh]
2377 let navRow = $('<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">').appendTo(calCard);
2378 let navLeft = $('<div style="display:flex;align-items:center;gap:8px;">').appendTo(navRow);
2379 let btnPrev = $('<button type="button" class="button button-small">&laquo;</button>').appendTo(navLeft);
2380 let monthLabel = $('<strong style="min-width:140px;text-align:center;">').appendTo(navLeft);
2381 let btnNext = $('<button type="button" class="button button-small">&raquo;</button>').appendTo(navLeft);
2382 let btnRefresh = $('<button type="button" class="button button-secondary"><span class="dashicons dashicons-update" style="vertical-align:middle;"></span></button>').appendTo(navRow);
2383
2384 // Auto-refresh
2385 let autoRefreshDiv = $('<div style="display:flex;align-items:center;gap:6px;">').appendTo(navRow);
2386 autoRefreshDiv.append('<span style="font-size:12px;color:#666;">' + __('Auto-refresh:', 'event-tickets-with-ticket-scanner') + '</span>');
2387 let autoRefreshSelect = $('<select style="padding:2px 20px 2px 4px;font-size:12px;">').appendTo(autoRefreshDiv);
2388 autoRefreshSelect.append('<option value="0">' + __('Off', 'event-tickets-with-ticket-scanner') + '</option>');
2389 autoRefreshSelect.append('<option value="10">10s</option>');
2390 autoRefreshSelect.append('<option value="30">30s</option>');
2391 autoRefreshSelect.append('<option value="60">1 min</option>');
2392 autoRefreshSelect.append('<option value="300">5 min</option>');
2393 let autoRefreshTimer = null;
2394 let autoRefreshCountdown = $('<span style="font-size:11px;color:#999;min-width:30px;">').appendTo(autoRefreshDiv);
2395
2396 function startAutoRefresh() {
2397 stopAutoRefresh();
2398 let interval = parseInt(autoRefreshSelect.val());
2399 if (interval <= 0) { autoRefreshCountdown.text(''); return; }
2400 let remaining = interval;
2401 autoRefreshCountdown.text(remaining + 's');
2402 autoRefreshTimer = setInterval(function() {
2403 remaining--;
2404 if (remaining <= 0) {
2405 loadCalendar();
2406 remaining = interval;
2407 }
2408 autoRefreshCountdown.text(remaining + 's');
2409 }, 1000);
2410 }
2411 function stopAutoRefresh() {
2412 if (autoRefreshTimer) { clearInterval(autoRefreshTimer); autoRefreshTimer = null; }
2413 autoRefreshCountdown.text('');
2414 }
2415 autoRefreshSelect.on('change', startAutoRefresh);
2416
2417 let calRow = $('<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start;">').appendTo(calCard);
2418 let calendarDiv = $('<div>').appendTo(calRow);
2419 let detailDiv = $('<div>').appendTo(calRow);
2420
2421 function getExcluded() {
2422 let excluded = [];
2423 productCheckboxes.find('input:not(:checked)').each(function() { excluded.push($(this).val()); });
2424 return excluded;
2425 }
2426
2427 function loadCalendar() {
2428 let monthNames = [
2429 __('January'), __('February'), __('March'), __('April'),
2430 __('May'), __('June'), __('July'), __('August'),
2431 __('September'), __('October'), __('November'), __('December')
2432 ];
2433 monthLabel.text(monthNames[calMonth] + ' ' + calYear);
2434 calendarDiv.html(_getSpinnerHTML());
2435 detailDiv.html('');
2436
2437 let first = calYear + '-' + String(calMonth + 1).padStart(2, '0') + '-01';
2438 let last = calYear + '-' + String(calMonth + 1).padStart(2, '0') + '-' + String(new Date(calYear, calMonth + 1, 0).getDate()).padStart(2, '0');
2439
2440 _makeGet('getAllDaychooserCalendarData', {
2441 date_from: first,
2442 date_to: last,
2443 exclude_products: getExcluded().join(',')
2444 }, function(data) {
2445 renderCalendarGrid(calendarDiv, detailDiv, data);
2446 }, function() {
2447 calendarDiv.html('<p style="color:red;">' + __('Error loading data.', 'event-tickets-with-ticket-scanner') + '</p>');
2448 });
2449 }
2450
2451 btnPrev.on('click', function() { calMonth--; if (calMonth < 0) { calMonth = 11; calYear--; } loadCalendar(); });
2452 btnNext.on('click', function() { calMonth++; if (calMonth > 11) { calMonth = 0; calYear++; } loadCalendar(); });
2453 btnRefresh.on('click', loadCalendar);
2454
2455 // Load product list, then load calendar
2456 _makeGet('getAllDaychooserCalendarData', {date_from: '2099-01-01', date_to: '2099-01-01'}, function(data) {
2457 if (data.products) {
2458 data.products.forEach(function(p) {
2459 let lbl = $('<label style="font-size:13px;cursor:pointer;">');
2460 let cb = $('<input type="checkbox" value="' + p.id + '" checked>');
2461 lbl.append(cb).append(' ' + p.name);
2462 productCheckboxes.append(lbl);
2463 });
2464 }
2465 loadCalendar();
2466 });
2467 }
2468
2469 function renderCalendarGrid(calendarDiv, detailDiv, data) {
2470 let allDates = data.dates || {};
2471 let dateFrom = data.date_from;
2472 let dateTo = data.date_to;
2473
2474 let startD = new Date(dateFrom + 'T00:00:00');
2475 let endD = new Date(dateTo + 'T00:00:00');
2476 let currentMonth = {year: startD.getFullYear(), month: startD.getMonth()};
2477 let endMonth = {year: endD.getFullYear(), month: endD.getMonth()};
2478
2479 let monthNames = [
2480 __('January', 'event-tickets-with-ticket-scanner'), __('February', 'event-tickets-with-ticket-scanner'),
2481 __('March', 'event-tickets-with-ticket-scanner'), __('April', 'event-tickets-with-ticket-scanner'),
2482 __('May', 'event-tickets-with-ticket-scanner'), __('June', 'event-tickets-with-ticket-scanner'),
2483 __('July', 'event-tickets-with-ticket-scanner'), __('August', 'event-tickets-with-ticket-scanner'),
2484 __('September', 'event-tickets-with-ticket-scanner'), __('October', 'event-tickets-with-ticket-scanner'),
2485 __('November', 'event-tickets-with-ticket-scanner'), __('December', 'event-tickets-with-ticket-scanner')
2486 ];
2487 let dayHeaders = [
2488 __('Mon', 'event-tickets-with-ticket-scanner'), __('Tue', 'event-tickets-with-ticket-scanner'),
2489 __('Wed', 'event-tickets-with-ticket-scanner'), __('Thu', 'event-tickets-with-ticket-scanner'),
2490 __('Fri', 'event-tickets-with-ticket-scanner'), __('Sat', 'event-tickets-with-ticket-scanner'),
2491 __('Sun', 'event-tickets-with-ticket-scanner')
2492 ];
2493
2494 let $wrapper = $('<div>');
2495 let grandTotal = 0;
2496
2497 // Render each month in the range
2498 while (currentMonth.year < endMonth.year || (currentMonth.year === endMonth.year && currentMonth.month <= endMonth.month)) {
2499 let year = currentMonth.year;
2500 let month = currentMonth.month;
2501
2502 let $monthBlock = $('<div style="margin-bottom:24px;">');
2503 $monthBlock.append('<h4 style="margin:0 0 8px;">' + monthNames[month] + ' ' + year + '</h4>');
2504
2505 let $table = $('<table class="widefat" style="table-layout:fixed;text-align:center;">');
2506 let $thead = $('<thead><tr></tr></thead>');
2507 for (let i = 0; i < 7; i++) {
2508 $thead.find('tr').append('<th style="text-align:center;padding:4px;">' + dayHeaders[i] + '</th>');
2509 }
2510 $table.append($thead);
2511
2512 let $tbody = $('<tbody>');
2513 let firstDay = new Date(year, month, 1).getDay();
2514 let startOffset = (firstDay === 0 ? 6 : firstDay - 1);
2515 let daysInMonth = new Date(year, month + 1, 0).getDate();
2516 let day = 1;
2517
2518 for (let row = 0; row < 6 && day <= daysInMonth; row++) {
2519 let $tr = $('<tr>');
2520 for (let col = 0; col < 7; col++) {
2521 if ((row === 0 && col < startOffset) || day > daysInMonth) {
2522 $tr.append('<td style="padding:4px;"></td>');
2523 } else {
2524 let dateStr = year + '-' + String(month + 1).padStart(2, '0') + '-' + String(day).padStart(2, '0');
2525 let dayData = allDates[dateStr] || [];
2526 let dayTotal = 0;
2527 dayData.forEach(function(d) { dayTotal += d.count; });
2528 grandTotal += dayTotal;
2529
2530 let $td = $('<td style="padding:4px;position:relative;vertical-align:top;height:45px;">');
2531 $td.append('<div style="font-size:11px;color:#666;">' + day + '</div>');
2532 if (dayTotal > 0) {
2533 let $badge = $('<div style="font-weight:bold;color:#0073aa;cursor:pointer;">' + dayTotal + '</div>');
2534 $badge.data('cal-date', dateStr);
2535 $badge.data('cal-products', dayData);
2536 $badge.on('click', function(e) {
2537 e.preventDefault();
2538 let ds = $(this).data('cal-date');
2539 let dd = $(this).data('cal-products');
2540
2541 detailDiv.html('<h4 style="margin:0 0 8px;">' + sprintf(__('Tickets for %s', 'event-tickets-with-ticket-scanner'), ds) + '</h4>' + _getSpinnerHTML());
2542
2543 // Load details for each product
2544 let promises = dd.map(function(p) {
2545 return new Promise(function(resolve) {
2546 _makeGet('getProductCalendarDetails', {product_id: p.product_id, date: ds}, function(tickets) {
2547 resolve({product: p, tickets: tickets || []});
2548 }, function() {
2549 resolve({product: p, tickets: []});
2550 });
2551 });
2552 });
2553
2554 Promise.all(promises).then(function(results) {
2555 let $detail = $('<div>');
2556 $detail.append('<h4 style="margin:0 0 8px;">' + sprintf(__('Tickets for %s', 'event-tickets-with-ticket-scanner'), ds) + '</h4>');
2557
2558 results.forEach(function(r) {
2559 $detail.append('<strong>' + r.product.product_name + ' (' + r.product.count + ')</strong>');
2560 if (r.tickets.length === 0) {
2561 $detail.append('<p><em>' + __('No details available.', 'event-tickets-with-ticket-scanner') + '</em></p>');
2562 return;
2563 }
2564 let $table = $('<table class="widefat striped" style="margin:4px 0 16px;">');
2565 $table.append('<thead><tr>'
2566 + '<th>' + __('Ticket', 'event-tickets-with-ticket-scanner') + '</th>'
2567 + '<th>' + __('Order', 'event-tickets-with-ticket-scanner') + '</th>'
2568 + '<th>' + __('Status', 'event-tickets-with-ticket-scanner') + '</th>'
2569 + '</tr></thead>');
2570 let $tbody = $('<tbody>');
2571 r.tickets.forEach(function(t) {
2572 let ticketHtml = t.code_display;
2573 let orderHtml = t.order_id > 0
2574 ? '<a href="post.php?post=' + t.order_id + '&action=edit" target="_blank">#' + t.order_id + '</a>'
2575 : '-';
2576 let statusText = t.redeemed ? __('Redeemed', 'event-tickets-with-ticket-scanner') : __('Active', 'event-tickets-with-ticket-scanner');
2577 let statusColor = t.redeemed ? '#d63638' : '#00a32a';
2578 $tbody.append('<tr><td>' + ticketHtml + '</td><td>' + orderHtml + '</td><td style="color:' + statusColor + ';">' + statusText + '</td></tr>');
2579 });
2580 $table.append($tbody);
2581 $detail.append($table);
2582 });
2583 detailDiv.empty().append($detail);
2584 });
2585 });
2586 $td.append($badge);
2587 $td.css('background', '#f0f7ff');
2588 }
2589 $tr.append($td);
2590 day++;
2591 }
2592 }
2593 $tbody.append($tr);
2594 }
2595 $table.append($tbody);
2596 $monthBlock.append($table);
2597 $wrapper.append($monthBlock);
2598
2599 // Next month
2600 currentMonth.month++;
2601 if (currentMonth.month > 11) { currentMonth.month = 0; currentMonth.year++; }
2602 }
2603
2604 let totalText = grandTotal > 0
2605 ? sprintf(__('Total sold: %d tickets', 'event-tickets-with-ticket-scanner'), grandTotal)
2606 : __('No tickets found for this month.', 'event-tickets-with-ticket-scanner');
2607 $wrapper.append('<div style="margin-top:8px;font-size:12px;color:#666;">' + totalText + '</div>');
2608
2609 calendarDiv.empty().append($wrapper);
2610 }
2611
2612 function _buildRedemptionTab(container) {
2613
2614 // Date range filter + Export button
2615 let today = new Date().toISOString().split('T')[0];
2616 let div_filters = $('<div class="et-card"/>').css({'display':'flex','gap':'15px','align-items':'flex-end','flex-wrap':'wrap'}).appendTo(container);
2617
2618 let div_from = _createDivInput(_x('From', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div_filters);
2619 let input_from = $('<input type="date"/>').val(today).css('padding','4px 8px').appendTo(div_from);
2620
2621 let div_to = _createDivInput(_x('To', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div_filters);
2622 let input_to = $('<input type="date"/>').val(today).css('padding','4px 8px').appendTo(div_to);
2623
2624 let btn_load = $('<button/>').addClass("button-primary").html(_x('Load', 'button', 'event-tickets-with-ticket-scanner')).appendTo(div_filters);
2625
2626 let btn_export = $('<button/>').addClass("button-secondary").html('<span class="dashicons dashicons-download" style="vertical-align:middle;margin-right:2px;"></span>' + _x('Export CSV', 'button', 'event-tickets-with-ticket-scanner')).appendTo(div_filters);
2627 btn_export.on("click", function() {
2628 let df = input_from.val(), dt = input_to.val();
2629 if (!df || !dt) return;
2630 window.open(_requestURL('downloadRedemptionSummary', {date_from: df, date_to: dt}), '_blank');
2631 });
2632
2633 // Summary cards
2634 let div_summary = $('<div/>').css({'display':'flex','gap':'20px','margin-bottom':'15px'}).appendTo(container);
2635 let cardStyle = {'padding':'15px','background':'white','border-radius':'5px','flex':'1','text-align':'center','border':'1px solid #c3c4c7'};
2636 let card_redeemed = $('<div/>').css(cardStyle).appendTo(div_summary);
2637 let card_total = $('<div/>').css(cardStyle).appendTo(div_summary);
2638 let card_noshow = $('<div/>').css(cardStyle).appendTo(div_summary);
2639
2640 // Table area
2641 let div_table = $('<div/>').css({'background':'white','padding':'15px','border-radius':'5px','border':'1px solid #c3c4c7'}).appendTo(container);
2642
2643 let tabelle_attendance_datatable = null;
2644
2645 function __loadData() {
2646 let date_from = input_from.val();
2647 let date_to = input_to.val();
2648 if (!date_from || !date_to) return;
2649
2650 div_table.html(_getSpinnerHTML());
2651 card_redeemed.html('...');
2652 card_total.html('...');
2653 card_noshow.html('...');
2654
2655 _makeGet('getRedemptionSummary', {date_from: date_from, date_to: date_to}, function(data) {
2656 // Summary cards
2657 card_redeemed.html(
2658 '<h2 style="margin:0;color:#2e74b5;">' + data.summary.total_redeemed + '</h2>' +
2659 '<p style="margin:5px 0 0;">' + _x('Redeemed', 'label', 'event-tickets-with-ticket-scanner') + '</p>'
2660 );
2661 card_total.html(
2662 '<h2 style="margin:0;color:#666;">' + data.summary.total_codes + '</h2>' +
2663 '<p style="margin:5px 0 0;">' + _x('Total Tickets', 'label', 'event-tickets-with-ticket-scanner') + '</p>'
2664 );
2665 card_noshow.html(
2666 '<h2 style="margin:0;color:#d63638;">' + data.summary.total_no_show + '</h2>' +
2667 '<p style="margin:5px 0 0;">' + _x('No Show', 'label', 'event-tickets-with-ticket-scanner') + '</p>'
2668 );
2669
2670 // Build DataTable
2671 let table_id = myAjax.divPrefix + '_tabelle_attendance';
2672 let tabelle = $('<table/>').attr("id", table_id);
2673 tabelle.html(
2674 '<thead><tr>' +
2675 '<th></th>' +
2676 '<th>' + _x('Date', 'column', 'event-tickets-with-ticket-scanner') + '</th>' +
2677 '<th>' + _x('Event / List', 'column', 'event-tickets-with-ticket-scanner') + '</th>' +
2678 '<th>' + _x('Product', 'column', 'event-tickets-with-ticket-scanner') + '</th>' +
2679 '<th>' + _x('Redeemed', 'column', 'event-tickets-with-ticket-scanner') + '</th>' +
2680 '<th>' + _x('Total in List', 'column', 'event-tickets-with-ticket-scanner') + '</th>' +
2681 '<th>' + _x('No Show', 'column', 'event-tickets-with-ticket-scanner') + '</th>' +
2682 '</tr></thead>' +
2683 '<tfoot><tr><th></th><th></th><th></th><th></th><th></th><th></th><th></th></tr></tfoot>'
2684 );
2685 div_table.html(tabelle);
2686
2687 if (tabelle_attendance_datatable) {
2688 try { tabelle_attendance_datatable.destroy(); } catch(e) {}
2689 }
2690
2691 tabelle_attendance_datatable = $('#' + table_id).DataTable({
2692 "responsive": true,
2693 "searching": true,
2694 "ordering": true,
2695 "processing": false,
2696 "serverSide": false,
2697 "stateSave": false,
2698 "paging": data.rows.length > 25,
2699 "pageLength": 25,
2700 "data": data.rows,
2701 "order": [[1, "desc"], [2, "asc"]],
2702 "columns": [
2703 {"data": null, "className": "details-control", "orderable": false, "defaultContent": "", "width": 10},
2704 {"data": "date", "orderable": true},
2705 {"data": "list_name", "orderable": true, "render": function(d){ return d || '-'; }},
2706 {"data": "product_name", "orderable": true, "render": function(d, type, row){
2707 return row.product_id > 0 ? d + ' <small>(#' + row.product_id + ')</small>' : (d || '-');
2708 }},
2709 {"data": "redeemed_count", "orderable": true, "className": "dt-right"},
2710 {"data": "total_in_list", "orderable": true, "className": "dt-right"},
2711 {"data": "no_show_count", "orderable": true, "className": "dt-right"}
2712 ],
2713 "footerCallback": function() {
2714 let api = this.api();
2715 let sumRedeemed = api.column(4).data().reduce(function(a, b){ return a + b; }, 0);
2716 let sumTotal = api.column(5).data().reduce(function(a, b){ return a + b; }, 0);
2717 let sumNoShow = api.column(6).data().reduce(function(a, b){ return a + b; }, 0);
2718 $(api.column(0).footer()).html('');
2719 $(api.column(1).footer()).html('<b>' + _x('Total', 'label', 'event-tickets-with-ticket-scanner') + '</b>');
2720 $(api.column(2).footer()).html('');
2721 $(api.column(3).footer()).html('');
2722 $(api.column(4).footer()).html('<b>' + sumRedeemed + '</b>');
2723 $(api.column(5).footer()).html('<b>' + sumTotal + '</b>');
2724 $(api.column(6).footer()).html('<b>' + sumNoShow + '</b>');
2725 }
2726 });
2727 tabelle.css("width", "100%");
2728
2729 // Drill-down: click details-control to show individual codes
2730 $('#' + table_id + ' tbody').on('click', 'td.details-control', function() {
2731 let tr = $(this).closest('tr');
2732 let row = tabelle_attendance_datatable.row(tr);
2733 if (row.child.isShown()) {
2734 row.child.hide();
2735 tr.removeClass('shown');
2736 } else {
2737 let d = row.data();
2738 let childDiv = $('<div/>').css('padding', '5px 10px').html(_getSpinnerHTML());
2739 row.child(childDiv).show();
2740 tr.addClass('shown');
2741
2742 _makeGet('getRedemptionDetails', {date: d.date, list_id: d.list_id}, function(details) {
2743 if (!details || details.length === 0) {
2744 childDiv.html('<p><i>' + _x('No individual ticket data available.', 'label', 'event-tickets-with-ticket-scanner') + '</i></p>');
2745 return;
2746 }
2747 let html = '<table class="widefat striped" style="margin:5px 0;">' +
2748 '<thead><tr>' +
2749 '<th>' + _x('Ticket', 'column', 'event-tickets-with-ticket-scanner') + '</th>' +
2750 '<th>' + _x('Order', 'column', 'event-tickets-with-ticket-scanner') + '</th>' +
2751 '<th>' + _x('Redeemed At', 'column', 'event-tickets-with-ticket-scanner') + '</th>' +
2752 '</tr></thead><tbody>';
2753 for (let i = 0; i < details.length; i++) {
2754 let item = details[i];
2755 let orderLink = item.order_id > 0
2756 ? '#' + item.order_id + ' <a target="_blank" href="post.php?post=' + item.order_id + '&action=edit">' + _x('View', 'label', 'event-tickets-with-ticket-scanner') + '</a>'
2757 : '-';
2758 html += '<tr><td>' + item.code_display + '</td><td>' + orderLink + '</td><td>' + item.redeemed_time + '</td></tr>';
2759 }
2760 html += '</tbody></table>';
2761 childDiv.html(html);
2762 }, function() {
2763 childDiv.html('<p style="color:red;">' + _x('Error loading details.', 'label', 'event-tickets-with-ticket-scanner') + '</p>');
2764 });
2765 }
2766 });
2767 }, function(response) {
2768 div_table.html('<p style="color:red;">' + (response.data || _x('Error loading data.', 'label', 'event-tickets-with-ticket-scanner')) + '</p>');
2769 card_redeemed.html('-');
2770 card_total.html('-');
2771 card_noshow.html('-');
2772 });
2773 }
2774
2775 btn_load.on("click", __loadData);
2776 __loadData();
2777 }
2778
2779 // ── End Attendance ──
2780
2781 function getSuffixFromFilename(filename) {
2782 let extension = filename.slice(filename.lastIndexOf('.') + 1);
2783 return extension;
2784 }
2785 function _renderMedia(mediaId, v, image_info, image, image_btn_del) {
2786 if (mediaId != "" && parseInt(mediaId) != 0) {
2787 _getMediaData(mediaId, data=>{
2788 let suffix = getSuffixFromFilename(data.url.replace(/^.*[\\\/]/,'')).toLowerCase();
2789 let info = suffix != "pdf" ? '('+data.meta.width+'x'+data.meta.height+')' : '';
2790 image_info.html('<b>'+_x('Title', 'title', 'event-tickets-with-ticket-scanner')+':</b> '+data.title+' '+info);
2791 if (v.additional.max && v.additional.msg_error_max) {
2792 if (v.additional.max.width && v.additional.msg_error_max.width && data.meta.width > v.additional.max.width) image_info.append('<div style="color:red;">'+v.additional.msg_error_max.width+'</div>');
2793 if (v.additional.max.height && v.additional.msg_error_max.height && data.meta.height > v.additional.max.height) image_info.append('<div style="color:red;">'+v.additional.msg_error_max.height+'</div>');
2794 }
2795 if (suffix != "pdf") {
2796 image.attr("src", data.url).css("display","block");
2797 }
2798 image_btn_del.css("display", "block");
2799 });
2800 } else {
2801 image_info.html("");
2802 image.css("display", "none");
2803 image_btn_del.css("display", "none");
2804 }
2805 }
2806 function _initVtextEditors() {
2807 if (typeof wp === "undefined" || !wp.oldEditor) return;
2808 $('textarea[data-vtext-key]').each(function(){
2809 let $ta = $(this);
2810 let editorId = $ta.attr("id");
2811 let key = $ta.attr("data-vtext-key");
2812 let tagGroups = $ta.data("vtextTags") || null;
2813 if ($ta.data("vtext-init")) return; // already initialised
2814 $ta.data("vtext-init", true);
2815 // Drop any stale TinyMCE instance with the same id (options area re-render)
2816 wp.oldEditor.remove(editorId);
2817
2818 let toolbar1 = 'formatselect,bold,italic,underline,bullist,numlist,blockquote,alignleft,aligncenter,alignright,link,unlink,forecolor,removeformat';
2819 if (tagGroups && tagGroups.length) toolbar1 += ',etInsertTag';
2820
2821 wp.oldEditor.initialize(editorId, {
2822 tinymce: {
2823 toolbar1: toolbar1,
2824 height: 200,
2825 setup: function(ed){
2826 let _save = ()=>{
2827 let value = ed.getContent();
2828 _saveOptionValue(key, value);
2829 };
2830 // Save on blur and after content settles (debounced)
2831 ed.on('blur', _save);
2832 let _t = null;
2833 ed.on('change keyup', ()=>{
2834 window.clearTimeout(_t);
2835 _t = window.setTimeout(_save, 1200);
2836 });
2837 // Tag picker menubutton (grouped submenus)
2838 if (tagGroups && tagGroups.length) {
2839 let menuItems = tagGroups.map(g=>{
2840 return {
2841 text: g.group,
2842 menu: g.tags.map(tag=>({
2843 text: tag,
2844 onclick: function(){ ed.insertContent(tag); }
2845 }))
2846 };
2847 });
2848 ed.addButton('etInsertTag', {
2849 type: 'menubutton',
2850 text: __('Insert Tag', 'event-tickets-with-ticket-scanner'),
2851 icon: false,
2852 menu: menuItems
2853 });
2854 }
2855 }
2856 },
2857 quicktags: true,
2858 mediaButtons: false
2859 });
2860 });
2861 }
2862 function _openMediaChooser(input_elem, multiple, imgContainer, typeFilter) {
2863 var image_frame;
2864 if(image_frame){
2865 image_frame.open();
2866 }
2867 if (!typeFilter) typeFilter = 'image';
2868 multiple ? multiple = true : multiple = false;
2869 // Define image_frame as wp.media object
2870 image_frame = wp.media({
2871 title: _x('Select Media', 'title', 'event-tickets-with-ticket-scanner'),
2872 multiple : multiple,
2873 library : {
2874 type : typeFilter,
2875 }
2876 });
2877
2878 image_frame.on('close',function() {
2879 // On close, get selections and save to the hidden input
2880 // plus other AJAX stuff to refresh the image preview
2881 var selection = image_frame.state().get('selection');
2882
2883 if (imgContainer) { // zeige erstes bild an
2884 var attachment = selection.first().toJSON();
2885 imgContainer.html( '<img src="'+attachment.url+'" style="max-width:100%;"/>' );
2886 }
2887
2888 var gallery_ids = new Array();
2889 var my_index = 0;
2890 selection.each(function(attachment) {
2891 gallery_ids[my_index] = attachment['id'];
2892 my_index++;
2893 });
2894 var ids = gallery_ids.join(",");
2895 input_elem.val(ids);
2896 input_elem.trigger("change");
2897 });
2898
2899 image_frame.on('open',function() {
2900 // On open, get the id from the hidden input
2901 // and select the appropiate images in the media manager
2902 var selection = image_frame.state().get('selection');
2903 var ids = input_elem.val().split(',');
2904 ids.forEach(function(id) {
2905 var attachment = wp.media.attachment(id);
2906 attachment.fetch();
2907 selection.add( attachment ? [ attachment ] : [] );
2908 });
2909 });
2910 image_frame.open();
2911 } // ende openmediachooser
2912
2913 function getBackButtonDiv() {
2914 let div_buttons = $('<div class="event-tickets-with-ticket-scanner-topbar">');
2915 let div = $('<div/>').addClass("event-tickets-with-ticket-scanner-topbar-left").append(
2916 $('<button />')
2917 .addClass("event-tickets-with-ticket-scanner-back-btn")
2918 .html('<span class="event-tickets-with-ticket-scanner-back-icon">&lt;</span> ' + _x('Back', 'label', 'event-tickets-with-ticket-scanner'))
2919 .on("click", ()=>{ LAYOUT.renderAdminPageLayout(); }
2920 )
2921 );
2922 div_buttons.append(div);
2923 div_buttons.append(_displaySettingAreaButton());
2924 return div_buttons;
2925 }
2926
2927 function _getTicketScannerURL() {
2928 let url = _getOptions_Infos_getByKey('ticket').ticket_scanner_path;
2929 let _urlpath = _getOptions_getValByKey("wcTicketCompatibilityModeURLPath");
2930 if (_urlpath != "") {
2931 url = OPTIONS.infos.site.home+"/"+_urlpath+'/scanner/?code=';
2932 } else {
2933 url = OPTIONS.infos.ticket.ticket_scanner_url;
2934 }
2935 return url;
2936 }
2937 function _displaySettingAreaButton() {
2938 let btn_grp = $('<nav id="topMenu"/>')
2939 .addClass("event-tickets-with-ticket-scanner-topmenu")
2940 .attr("aria-label", "Event Tickets navigation");
2941 $('<button/>')
2942 .addClass('event-tickets-with-ticket-scanner-topmenu-item')
2943 .toggleClass('event-tickets-with-ticket-scanner-topmenu-item-active', STATE === 'support')
2944 .html(_x("Support Info", 'label', 'event-tickets-with-ticket-scanner'))
2945 .on("click", () => {
2946 _displaySupportInfoArea();
2947 })
2948 .appendTo(btn_grp);
2949 $('<button/>')
2950 .addClass("event-tickets-with-ticket-scanner-topmenu-item")
2951 .toggleClass('event-tickets-with-ticket-scanner-topmenu-item-active', STATE === 'faq')
2952 .html(_x("FAQ", 'label', 'event-tickets-with-ticket-scanner'))
2953 .on("click", ()=>{
2954 _displayFAQArea();
2955 }).appendTo(btn_grp);
2956 //if (_getOptions_Versions_isActivatedByKey('is_wc_available')) {
2957 $('<button/>').addClass("event-tickets-with-ticket-scanner-topmenu-item").html(_x("Ticket Scanner", 'label', 'event-tickets-with-ticket-scanner'))
2958 .on("click", ()=>{
2959 let url = _getTicketScannerURL();
2960 window.open(url, 'ticketscanner');
2961 })
2962 .appendTo(btn_grp);
2963 //}
2964 $('<button/>')
2965 .addClass("event-tickets-with-ticket-scanner-topmenu-item")
2966 .toggleClass('event-tickets-with-ticket-scanner-topmenu-item-active', STATE === 'authtokens')
2967 .html(_x('Auth Token', 'label', 'event-tickets-with-ticket-scanner'))
2968 .on("click", ()=>{
2969 _displayAuthTokensArea();
2970 }).appendTo(btn_grp);
2971 if (isPremium()) {
2972 $('<button/>')
2973 .addClass("event-tickets-with-ticket-scanner-topmenu-item")
2974 .toggleClass('event-tickets-with-ticket-scanner-topmenu-item-active', STATE === 'attendance')
2975 .html(_x('Attendance', 'label', 'event-tickets-with-ticket-scanner'))
2976 .on("click", ()=>{
2977 _displayAttendanceArea();
2978 }).appendTo(btn_grp);
2979 }
2980 $('<button/>')
2981 .addClass("event-tickets-with-ticket-scanner-topmenu-item")
2982 .toggleClass('event-tickets-with-ticket-scanner-topmenu-item-active', STATE === 'congresses')
2983 .html(_x('Congresses', 'label', 'event-tickets-with-ticket-scanner'))
2984 .on("click", () => {
2985 _displayCongressesArea();
2986 }).appendTo(btn_grp);
2987
2988 $('<button/>')
2989 .addClass("event-tickets-with-ticket-scanner-topmenu-item")
2990 .toggleClass('event-tickets-with-ticket-scanner-topmenu-item-active', STATE === 'options')
2991 .html(_x('Options', 'label', 'event-tickets-with-ticket-scanner'))
2992 .on("click", ()=>{
2993 _displayOptionsArea();
2994 }).appendTo(btn_grp);
2995
2996 if (isPremium()) {
2997 btn_grp = PREMIUM.displaySettingAreaButton(btn_grp);
2998 }
2999 return btn_grp;
3000 }
3001
3002 function _form_fields_serial_format(appendToDiv) {
3003 let input_prefix_codes;
3004 let input_type_codes;
3005 let input_amount_letters;
3006 let input_letter_excl;
3007 let input_letter_style;
3008 let input_include_numbers;
3009 let input_serial_delimiter;
3010 let input_serial_delimiter_space;
3011 let input_number_start;
3012 let input_number_offset;
3013
3014 let noNumbersOptions = false;
3015 let cbk = null;
3016 let formatterValues;
3017
3018 function _setNoNumberOptions() {
3019 noNumbersOptions = true;
3020 }
3021 function _setCallbackHandle(_cbk) {
3022 cbk = _cbk;
3023 }
3024 function _callCallbackHandle() {
3025 cbk && cbk(_getFormatterValues());
3026 }
3027 function _setFormatterValues(values) {
3028 formatterValues = values;
3029 }
3030
3031 function __render() {
3032 $('<br>').appendTo(appendToDiv);
3033 // prefix
3034 let div_prefix_codes = _createDivInput(_x("Enter a prefix (optional)", 'label', 'event-tickets-with-ticket-scanner')).appendTo(appendToDiv);
3035 input_prefix_codes = $('<input type="text">').appendTo(div_prefix_codes);
3036 $('<div>').html(__('You can use date placeholder to have the prefix filled with the date of the confirmed purchase.', 'event-tickets-with-ticket-scanner')+'<br>'+__('You can use: {Y} = year, {m} = month, {d} = day, {H} = hour, {i} = minutes, {s} = seconds, {TIMESTAMP} = unix timestamp.', 'event-tickets-with-ticket-scanner')).appendTo(div_prefix_codes);
3037 if (formatterValues && formatterValues['input_prefix_codes'] != null) input_prefix_codes.val(formatterValues['input_prefix_codes']);
3038 input_prefix_codes.on("change", ()=>{
3039 _callCallbackHandle();
3040 });
3041 // type numbers/serials
3042 let div_type_codes = _createDivInput(_x("Choose type of ticket numbers", 'label', 'event-tickets-with-ticket-scanner')).appendTo(appendToDiv);
3043 input_type_codes = $('<select><option value="1" selected>'+_x('Serials', 'option value', 'event-tickets-with-ticket-scanner')+'</option><option value="2">'+_x('Numbers', 'option value', 'event-tickets-with-ticket-scanner')+'</option></select>').appendTo(div_type_codes);
3044 if (formatterValues && formatterValues['input_type_codes'] != null) input_type_codes.val(formatterValues['input_type_codes']);
3045
3046 if (noNumbersOptions) {
3047 input_type_codes.prop("disabled", true);
3048 }
3049 input_type_codes.on("change", function() {
3050 if (input_type_codes.val() === "2") {
3051 div_serials && div_serials.find("input").prop("disabled", true);
3052 div_serials && div_serials.find("select").prop("disabled", true);
3053 div_numbers && div_numbers.find("input").prop("disabled", false);
3054 div_numbers && div_numbers.find("select").prop("disabled", false);
3055 } else {
3056 div_serials && div_serials.find("input").prop("disabled", false);
3057 div_serials && div_serials.find("select").prop("disabled", false);
3058 div_numbers && div_numbers.find("input").prop("disabled", true);
3059 div_numbers && div_numbers.find("select").prop("disabled", true);
3060 }
3061 _callCallbackHandle();
3062 });
3063 // serials options
3064 let div_serials = $('<div>').html('<h4>'+_x('Serials options', 'title', 'event-tickets-with-ticket-scanner')+'</h4>').appendTo(appendToDiv);
3065 // anzahl letters
3066 let div_amount_letters = _createDivInput(_x('Amount of letter needed', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div_serials);
3067 input_amount_letters = $('<input type="number" required value="21" min="1" max="30">').appendTo(div_amount_letters);
3068 if (formatterValues && formatterValues['input_amount_letters'] != null) input_amount_letters.val(formatterValues['input_amount_letters']);
3069 input_amount_letters.on("change", function(){
3070 input_serial_delimiter.trigger("change");
3071 _callCallbackHandle();
3072 });
3073 // select letter exclusion
3074 let div_letter_excl = _createDivInput(_x('Letter exclusion', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div_serials);
3075 input_letter_excl = $('<select><option value="1">'+_x('None', 'option value', 'event-tickets-with-ticket-scanner')+'</option><option value="2" selected>i,l,o,p,q</option></select>').appendTo(div_letter_excl);
3076 if (formatterValues && formatterValues['input_letter_excl'] != null) input_letter_excl.val(formatterValues['input_letter_excl']);
3077 input_letter_excl.on("change", ()=>{
3078 _callCallbackHandle();
3079 });
3080 // radio button text gross/klein/both/none
3081 let div_letter_style = _createDivInput(_x('Letter style', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div_serials);
3082 input_letter_style = $('<select><option value="1" selected>'+_x('Uppercase', 'option value', 'event-tickets-with-ticket-scanner')+'</option><option value="2">'+_x('Lowercase', 'option value', 'event-tickets-with-ticket-scanner')+'</option><option value="3">'+_x('Both', 'option value', 'event-tickets-with-ticket-scanner')+'</option></select>').appendTo(div_letter_style);
3083 if (formatterValues && formatterValues['input_letter_style'] != null) input_letter_style.val(formatterValues['input_letter_style']);
3084 input_letter_style.on("change", ()=>{
3085 _callCallbackHandle();
3086 });
3087 // radio button numbers/none
3088 let div_include_numbers = _createDivInput(_x('Numbers needed?', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div_serials);
3089 input_include_numbers = $('<select><option value="1">'+_x('No', 'label', 'event-tickets-with-ticket-scanner')+'</option><option value="2" selected>'+_x('Yes', 'label', 'event-tickets-with-ticket-scanner')+'</option><option value="3">'+_x('Only numbers', 'option value', 'event-tickets-with-ticket-scanner')+'</option></select>').appendTo(div_include_numbers);
3090 if (formatterValues && formatterValues['input_include_numbers'] != null) input_include_numbers.val(formatterValues['input_include_numbers']);
3091 input_include_numbers.on("change", ()=>{
3092 _callCallbackHandle();
3093 });
3094 // select delimiter none/-/./space
3095 let div_serial_delimiter = _createDivInput(_x('Delimiter?', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div_serials);
3096 input_serial_delimiter = $('<select><option value="1">'+_x('None', 'option value', 'event-tickets-with-ticket-scanner')+'</option><option value="2" selected>-</option><option value="4">:</option><option value="3">'+_x('Space', 'option value', 'event-tickets-with-ticket-scanner')+'</option></select>').appendTo(div_serial_delimiter);
3097 if (formatterValues && formatterValues['input_serial_delimiter'] != null) input_serial_delimiter.val(formatterValues['input_serial_delimiter']);
3098 function __refreshDelimiterSpace() {
3099 input_serial_delimiter_space.html("");
3100 if (input_serial_delimiter.val() !== "1") {
3101 let anzahl = parseInt(input_amount_letters.val(),10);
3102 if (anzahl > 0) {
3103 for(let a=1;a<anzahl;a++) input_serial_delimiter_space.append($('<option'+(anzahl > 2 && a === 7 ? " selected": "")+'>').attr("value",a).html(a));
3104 }
3105 }
3106 }
3107 input_serial_delimiter.on("change", function(){
3108 __refreshDelimiterSpace();
3109 _callCallbackHandle();
3110 });
3111 // choose delimiter space
3112 let div_serial_delimiter_space = _createDivInput(_x('After how many letters?', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div_serials);
3113 input_serial_delimiter_space = $('<select></select>').appendTo(div_serial_delimiter_space);
3114 if (formatterValues && formatterValues['input_serial_delimiter'] != null) {
3115 // setze Werte erstmal ein
3116 __refreshDelimiterSpace();
3117 }
3118 if (formatterValues && formatterValues['input_serial_delimiter_space'] != null) input_serial_delimiter_space.val(formatterValues['input_serial_delimiter_space']);
3119 input_serial_delimiter_space.on("change", ()=>{
3120 _callCallbackHandle();
3121 });
3122 // numbers options
3123 let div_numbers = $('<div>').html('<h4>'+_x('Numbers options', 'title', 'event-tickets-with-ticket-scanner')+'</h4>').appendTo(appendToDiv);
3124 if (noNumbersOptions) div_numbers.css("display","none");
3125 // number start
3126 let div_number_start = _createDivInput(_x('Start number', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div_numbers);
3127 input_number_start = $('<input type="number" disabled required value="10000" min="1">').appendTo(div_number_start);
3128 if (formatterValues && formatterValues['input_number_start'] != null) input_number_start.val(formatterValues['input_number_start']);
3129 input_number_start.on("change", ()=>{
3130 _callCallbackHandle();
3131 });
3132 // number offset
3133 let div_number_offset = _createDivInput(_x('Offset for each number', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div_numbers);
3134 input_number_offset = $('<input type="number" disabled required value="1" min="1">').appendTo(div_number_offset);
3135 if (formatterValues && formatterValues['input_number_offset'] != null) input_number_offset.val(formatterValues['input_number_offset']);
3136 input_number_offset.on("change", ()=>{
3137 _callCallbackHandle();
3138 });
3139 }
3140
3141 function __generateCode(length, cases, withnumbers, exclusion) {
3142 let charset = 'abcdefghijklmnopqrstuvwxyz';
3143 if (cases === 1) charset = charset.toUpperCase();
3144 if (cases === 3) charset += charset.toUpperCase();
3145 if (withnumbers === 2) charset += '0123456789';
3146 if (withnumbers === 3) charset = '0123456789';
3147 if (typeof exclusion !== "undefined") {
3148 exclusion.forEach(function(v){
3149 let regex = new RegExp(v, 'gi');
3150 charset = charset.replace(regex, "");
3151 });
3152 }
3153 let retVal = "";
3154 for (var i = 0, n = charset.length; i < length; ++i) {
3155 retVal += charset.charAt(Math.floor(Math.random() * n));
3156 }
3157 return retVal;
3158 }
3159 function __insertSeperator(str, serial_delimiter, serial_delimiter_space) {
3160 if (str !== "" && serial_delimiter !== "" && serial_delimiter_space > 0) {
3161 let result = [str[0]];
3162 for(let x=1; x<str.length; x++) {
3163 if (x%serial_delimiter_space === 0) {
3164 result.push(serial_delimiter, str[x]);
3165 } else {
3166 result.push(str[x]);
3167 }
3168 }
3169 return result.join('');
3170 }
3171 return str;
3172 }
3173
3174 function _isTypeNumbers() {
3175 return input_type_codes.val() === "2";
3176 }
3177 function _getPrefix() {
3178 return input_prefix_codes.val().trim();
3179 }
3180 function _getAmountLetters() {
3181 let amount_letters = parseInt(input_amount_letters.val().trim(),10);
3182 if (isNaN(amount_letters) || amount_letters < 1) {
3183 input_amount_letters.select();
3184 return alert(__("Amount of letters has to be higher", 'event-tickets-with-ticket-scanner'));
3185 }
3186 return amount_letters;
3187 }
3188 function _getLetterExclusion() {
3189 return input_letter_excl.val() === "2" ? ['i','l','o','p','q'] : [];
3190 }
3191 function _getLetterStyle() {
3192 return parseInt(input_letter_style.val(),10);
3193 }
3194 function _getIncludeNumbers() {
3195 return parseInt(input_include_numbers.val(),10);
3196 }
3197 function _getSerialDelimiter() {
3198 return ['','-',' ',':'][parseInt(input_serial_delimiter.val(),10)-1];
3199 }
3200 function _getSerialDelimiterSpace() {
3201 let serial_delimiter_space = 0;
3202 try {
3203 serial_delimiter_space = _getSerialDelimiter() !== "" ? parseInt(input_serial_delimiter_space.val(),10) : 0;
3204 } catch (e) {}
3205 return serial_delimiter_space;
3206 }
3207 function _getNumberStart() {
3208 let start_number = parseInt(input_number_start.val().trim(),10);
3209 if (isNaN(start_number) || start_number < 1) {
3210 input_number_start.select();
3211 return alert(__("Your start number is not correct. It has to be an integer bigger than 0", 'event-tickets-with-ticket-scanner'));
3212 }
3213 return start_number;
3214 }
3215 function _getNumberOffset() {
3216 let number_offset = parseInt(input_number_offset.val().trim(),10);
3217 if (isNaN(number_offset) || number_offset < 1) number_offset = 1;
3218 return number_offset;
3219 }
3220 function _generateSerialCode(offsetCounter) {
3221 let code;
3222 let prefix = _getPrefix();
3223 if (_isTypeNumbers()) { // numbers
3224 if (!offsetCounter) offsetCounter = 0;
3225 let number_offset = offsetCounter * _getNumberOffset();
3226 code = _getNumberStart() + number_offset;
3227 if (prefix !== '') code = prefix + code;
3228 } else {
3229 code = __generateCode(_getAmountLetters(), _getLetterStyle(), _getIncludeNumbers(), _getLetterExclusion());
3230 code = __insertSeperator(code, _getSerialDelimiter(), _getSerialDelimiterSpace());
3231 if (prefix !== '') code = prefix + code;
3232 }
3233 return code;
3234 }
3235 function _getFormatterValues() {
3236 return {
3237 input_prefix_codes:_getPrefix().replace('/', '-'),
3238 input_type_codes:input_type_codes.val(),
3239 input_amount_letters:_getAmountLetters(),
3240 input_letter_excl:input_letter_excl.val(),
3241 input_letter_style:_getLetterStyle(),
3242 input_include_numbers:input_include_numbers.val(),
3243 input_serial_delimiter:input_serial_delimiter.val(),
3244 input_serial_delimiter_space:input_serial_delimiter_space.val(),
3245 input_number_start:_getNumberStart(),
3246 input_number_offset:_getNumberOffset()
3247 };
3248 }
3249
3250 return {
3251 render:__render,
3252 getAmountLetters:_getAmountLetters,
3253 getLetterExclusion:_getLetterExclusion,
3254 getLetterStyle:_getLetterStyle,
3255 getIncludeNumbers:_getIncludeNumbers,
3256 getSerialDelimiter:_getSerialDelimiter,
3257 getSerialDelimiterSpace:_getSerialDelimiterSpace,
3258 getNumberStart:_getNumberStart,
3259 getNumberOffset:_getNumberOffset,
3260 isTypeNumbers:_isTypeNumbers,
3261 getPrefix:_getPrefix,
3262 generateSerialCode:_generateSerialCode,
3263 setNoNumberOptions:_setNoNumberOptions,
3264 getFormatterValues:_getFormatterValues,
3265 setCallbackHandle:_setCallbackHandle,
3266 setFormatterValues:_setFormatterValues
3267 };
3268 }
3269
3270 function _createDivInput(label) {
3271 return $('<div/>').css({
3272 "display": "inline-block",
3273 "margin-bottom": "15px",
3274 "margin-right": "15px"
3275 }).html(label+"<br>");
3276 }
3277
3278 // ── Context-Wizards: smart suggestions on options page (#232) ──
3279
3280 var _sessionDismissedSuggestions = [];
3281
3282 function __getContextSuggestions() {
3283 return [
3284 // ── Email Cluster ──
3285 {
3286 id: 'email_ics_attach',
3287 trigger: function() {
3288 return _getOptions_isActivatedByKey('wcTicketAttachTicketToMail')
3289 && !_getOptions_isActivatedByKey('wcTicketAttachICSToMail');
3290 },
3291 targetKey: 'wcTicketAttachICSToMail',
3292 targetVal: 1,
3293 premium: true
3294 },
3295 {
3296 id: 'email_download_all_pdf',
3297 trigger: function() {
3298 return _getOptions_isActivatedByKey('wcTicketAttachTicketToMail')
3299 && !_getOptions_isActivatedByKey('wcTicketDisplayDownloadAllTicketsPDFButtonOnMail');
3300 },
3301 targetKey: 'wcTicketDisplayDownloadAllTicketsPDFButtonOnMail',
3302 targetVal: 1,
3303 premium: true
3304 },
3305 {
3306 id: 'email_badge_link',
3307 trigger: function() {
3308 return _getOptions_isActivatedByKey('wcTicketBadgeDisplayButtonOnDetail')
3309 && !_getOptions_isActivatedByKey('wcTicketBadgeAttachLinkToMail');
3310 },
3311 targetKey: 'wcTicketBadgeAttachLinkToMail',
3312 targetVal: 1,
3313 premium: false
3314 },
3315 {
3316 id: 'email_view_link_when_pdf_hidden',
3317 trigger: function() {
3318 return _getOptions_isActivatedByKey('wcTicketDontDisplayPDFButtonOnMail')
3319 && !_getOptions_isActivatedByKey('wcTicketDisplayOrderTicketsViewLinkOnMail');
3320 },
3321 targetKey: 'wcTicketDisplayOrderTicketsViewLinkOnMail',
3322 targetVal: 1,
3323 premium: false
3324 },
3325 // ── Scanner Cluster ──
3326 {
3327 id: 'scanner_auto_redeem',
3328 trigger: function() {
3329 return _getOptions_isActivatedByKey('ticketScannerStartCamWithoutButtonClicked')
3330 && !_getOptions_isActivatedByKey('ticketScannerScanAndRedeemImmediately');
3331 },
3332 targetKey: 'ticketScannerScanAndRedeemImmediately',
3333 targetVal: 1,
3334 premium: false
3335 },
3336 {
3337 id: 'scanner_vibrate',
3338 trigger: function() {
3339 return _getOptions_isActivatedByKey('ticketScannerScanAndRedeemImmediately')
3340 && !_getOptions_isActivatedByKey('ticketScannerVibrate');
3341 },
3342 targetKey: 'ticketScannerVibrate',
3343 targetVal: 1,
3344 premium: false
3345 },
3346 {
3347 id: 'scanner_confirmed_count',
3348 trigger: function() {
3349 return _getOptions_isActivatedByKey('wcTicketAllowRedeemTicketAfterEnd')
3350 && !_getOptions_isActivatedByKey('wcTicketScannerDisplayConfirmedCount');
3351 },
3352 targetKey: 'wcTicketScannerDisplayConfirmedCount',
3353 targetVal: 1,
3354 premium: false
3355 },
3356 // ── Ticket Display Cluster ──
3357 {
3358 id: 'display_date_on_product',
3359 trigger: function() {
3360 return _getOptions_isActivatedByKey('wcTicketDisplayDateOnMail')
3361 && !_getOptions_isActivatedByKey('wcTicketDisplayDateOnPrdDetail');
3362 },
3363 targetKey: 'wcTicketDisplayDateOnPrdDetail',
3364 targetVal: 1,
3365 premium: false
3366 },
3367 {
3368 id: 'display_customer_note',
3369 trigger: function() {
3370 return _getOptions_isActivatedByKey('wcTicketShowInputFieldsOnCheckoutPage')
3371 && !_getOptions_isActivatedByKey('wcTicketDisplayCustomerNote');
3372 },
3373 targetKey: 'wcTicketDisplayCustomerNote',
3374 targetVal: 1,
3375 premium: false
3376 },
3377 {
3378 id: 'display_redirect_after_redeem',
3379 trigger: function() {
3380 return _getOptions_isActivatedByKey('wcTicketShowRedeemBtnOnTicket')
3381 && !_getOptions_isActivatedByKey('wcTicketRedirectUser');
3382 },
3383 targetKey: 'wcTicketRedirectUser',
3384 targetVal: 1,
3385 premium: false
3386 },
3387 // ── Security Cluster (Premium) ──
3388 {
3389 id: 'security_ip_block',
3390 trigger: function() {
3391 return isPremium()
3392 && !_getOptions_isActivatedByKey('activateUserIPBlock');
3393 },
3394 targetKey: 'activateUserIPBlock',
3395 targetVal: 1,
3396 premium: true
3397 },
3398 {
3399 id: 'security_ip_tracking',
3400 trigger: function() {
3401 return isPremium()
3402 && !_getOptions_isActivatedByKey('trackIPCodeChecker');
3403 },
3404 targetKey: 'trackIPCodeChecker',
3405 targetVal: 1,
3406 premium: true
3407 },
3408 // ── Wallet ──
3409 {
3410 id: 'wallet_vollstart_enable',
3411 trigger: function() {
3412 return OPTIONS.infos && OPTIONS.infos.ticket && OPTIONS.infos.ticket.counter > 0
3413 && !_getOptions_isActivatedByKey('walletVollstartEnable');
3414 },
3415 targetKey: 'walletVollstartEnable',
3416 targetVal: 1,
3417 premium: false
3418 },
3419 // ── Eventado public calendar (Premium) ──
3420 {
3421 id: 'eventado_publish_enable',
3422 trigger: function() {
3423 return isPremium()
3424 && OPTIONS.mapKeys
3425 && OPTIONS.mapKeys.eventCalendarPublishEnable
3426 && !_getOptions_isActivatedByKey('eventCalendarPublishEnable');
3427 },
3428 targetKey: 'eventCalendarPublishEnable',
3429 targetVal: 1,
3430 premium: true
3431 }
3432 ];
3433 }
3434
3435 function __getSuggestionMessage(id) {
3436 var messages = {
3437 'email_ics_attach': {
3438 context: __('You have PDF ticket attachment enabled for emails.', 'event-tickets-with-ticket-scanner'),
3439 question: __('Do you also want to attach the ICS calendar file to the purchase emails?', 'event-tickets-with-ticket-scanner')
3440 },
3441 'email_download_all_pdf': {
3442 context: __('You have PDF ticket attachment enabled for emails.', 'event-tickets-with-ticket-scanner'),
3443 question: __('Do you also want to include a "Download all tickets as one PDF" link in the email?', 'event-tickets-with-ticket-scanner')
3444 },
3445 'email_badge_link': {
3446 context: __('You show the badge download button on the ticket detail page.', 'event-tickets-with-ticket-scanner'),
3447 question: __('Do you also want to include the badge download link in the purchase emails?', 'event-tickets-with-ticket-scanner')
3448 },
3449 'email_view_link_when_pdf_hidden': {
3450 context: __('You have hidden the PDF download button on purchase emails.', 'event-tickets-with-ticket-scanner'),
3451 question: __('Do you want to show the "View all tickets" link in the email instead?', 'event-tickets-with-ticket-scanner')
3452 },
3453 'scanner_auto_redeem': {
3454 context: __('You have auto-start camera enabled for the ticket scanner.', 'event-tickets-with-ticket-scanner'),
3455 question: __('Do you also want to enable automatic ticket redemption on scan?', 'event-tickets-with-ticket-scanner')
3456 },
3457 'scanner_vibrate': {
3458 context: __('You have automatic scan-and-redeem enabled.', 'event-tickets-with-ticket-scanner'),
3459 question: __('Do you also want to enable haptic feedback (vibration) when a ticket is scanned?', 'event-tickets-with-ticket-scanner')
3460 },
3461 'scanner_confirmed_count': {
3462 context: __('You allow tickets to be redeemed after the event end date.', 'event-tickets-with-ticket-scanner'),
3463 question: __('Do you also want to display the confirmed scan count on the ticket scanner?', 'event-tickets-with-ticket-scanner')
3464 },
3465 'display_date_on_product': {
3466 context: __('You show the event date on the purchase order email.', 'event-tickets-with-ticket-scanner'),
3467 question: __('Do you also want to show the event date on the product detail page?', 'event-tickets-with-ticket-scanner')
3468 },
3469 'display_customer_note': {
3470 context: __('You show input fields on the checkout page.', 'event-tickets-with-ticket-scanner'),
3471 question: __('Do you also want to display the customer note on the ticket PDF?', 'event-tickets-with-ticket-scanner')
3472 },
3473 'display_redirect_after_redeem': {
3474 context: __('You show the self-redeem button on the ticket detail page.', 'event-tickets-with-ticket-scanner'),
3475 question: __('Do you also want to redirect the customer after they redeem their ticket?', 'event-tickets-with-ticket-scanner')
3476 },
3477 'security_ip_block': {
3478 context: __('You have the premium plugin active.', 'event-tickets-with-ticket-scanner'),
3479 question: __('Do you want to enable IP blocking to protect against brute-force ticket validation attempts?', 'event-tickets-with-ticket-scanner')
3480 },
3481 'security_ip_tracking': {
3482 context: __('You have the premium plugin active.', 'event-tickets-with-ticket-scanner'),
3483 question: __('Do you want to enable IP tracking for ticket validation requests?', 'event-tickets-with-ticket-scanner')
3484 },
3485 'wallet_vollstart_enable': {
3486 context: __('Your customers can collect their tickets in the free Vollstart Wallet app.', 'event-tickets-with-ticket-scanner'),
3487 question: __('Do you want to enable the "Add to Vollstart Wallet" button on ticket pages and in emails?', 'event-tickets-with-ticket-scanner')
3488 },
3489 'eventado_publish_enable': {
3490 context: __('Eventado.com is a free public event calendar — Premium customers can list their ticket events automatically.', 'event-tickets-with-ticket-scanner'),
3491 question: __('Do you want to publish your ticket events to Eventado.com for more reach and SEO?', 'event-tickets-with-ticket-scanner')
3492 }
3493 };
3494 return messages[id] || {context: '', question: ''};
3495 }
3496
3497 function __renderContextSuggestions(container) {
3498 container.find('.saso-context-suggestions').remove();
3499
3500 var suggestions = __getContextSuggestions();
3501 var dismissed = (OPTIONS.dismissed_suggestions || []).concat(_sessionDismissedSuggestions);
3502
3503 var active = suggestions.filter(function(s) {
3504 if (s.premium && !isPremium()) return false;
3505 if (dismissed.indexOf(s.id) >= 0) return false;
3506 try { return s.trigger(); } catch(e) { return false; }
3507 });
3508
3509 active = active.slice(0, 3);
3510 if (active.length === 0) return;
3511
3512 if (!$('#saso-ctx-suggestions-styles').length) {
3513 $('<style id="saso-ctx-suggestions-styles"/>').text(
3514 '.saso-context-suggestions{margin:15px 0 20px 0;}' +
3515 '.saso-ctx-card{background:#fff;border:1px solid #c3c4c7;border-left:4px solid #dba617;padding:14px 18px;margin-bottom:10px;border-radius:4px;box-shadow:0 1px 1px rgba(0,0,0,.04);}' +
3516 '.saso-ctx-body{display:flex;align-items:flex-start;gap:10px;margin-bottom:10px;}' +
3517 '.saso-ctx-icon{color:#dba617;font-size:22px;flex-shrink:0;margin-top:2px;}' +
3518 '.saso-ctx-text{flex:1;}' +
3519 '.saso-ctx-context{font-size:13px;color:#646970;margin-bottom:3px;}' +
3520 '.saso-ctx-question{font-size:14px;color:#1d2327;font-weight:500;}' +
3521 '.saso-ctx-buttons{display:flex;gap:8px;align-items:center;}' +
3522 '.saso-ctx-btn-dismiss{color:#646970!important;text-decoration:none!important;}'
3523 ).appendTo('head');
3524 }
3525
3526 var wrap = $('<div class="saso-context-suggestions"/>');
3527
3528 active.forEach(function(s) {
3529 var msg = __getSuggestionMessage(s.id);
3530 var card = $('<div class="saso-ctx-card" data-suggestion-id="' + s.id + '"/>');
3531
3532 var body = $('<div class="saso-ctx-body"/>');
3533 $('<span class="saso-ctx-icon dashicons dashicons-lightbulb"/>').appendTo(body);
3534 var textWrap = $('<div class="saso-ctx-text"/>').appendTo(body);
3535 $('<div class="saso-ctx-context"/>').text(msg.context).appendTo(textWrap);
3536 $('<div class="saso-ctx-question"/>').text(msg.question).appendTo(textWrap);
3537 card.append(body);
3538
3539 var btnWrap = $('<div class="saso-ctx-buttons"/>');
3540
3541 $('<button class="button button-primary button-small"/>')
3542 .text(__('Yes, enable', 'event-tickets-with-ticket-scanner'))
3543 .on('click', function() {
3544 _makePost('changeOption', {'key': s.targetKey, 'value': s.targetVal}, function() {
3545 if (OPTIONS.mapKeys[s.targetKey]) {
3546 OPTIONS.mapKeys[s.targetKey].value = s.targetVal;
3547 }
3548 _sessionDismissedSuggestions.push(s.id);
3549 card.slideUp(300, function() {
3550 card.remove();
3551 var optionRow = $('[data-key="' + s.targetKey + '"]').closest('div');
3552 if (optionRow.length) {
3553 optionRow.css('background-color', '#e7f5e7');
3554 setTimeout(function() { optionRow.css('background-color', ''); }, 2000);
3555 }
3556 __renderContextSuggestions(container);
3557 });
3558 });
3559 }).appendTo(btnWrap);
3560
3561 $('<button class="button button-secondary button-small"/>')
3562 .text(__('No thanks', 'event-tickets-with-ticket-scanner'))
3563 .on('click', function() {
3564 _sessionDismissedSuggestions.push(s.id);
3565 card.slideUp(300, function() { card.remove(); __renderContextSuggestions(container); });
3566 }).appendTo(btnWrap);
3567
3568 $('<button class="button button-link button-small saso-ctx-btn-dismiss"/>')
3569 .text(__("Don't ask again", 'event-tickets-with-ticket-scanner'))
3570 .on('click', function() {
3571 _makePost('dismissSuggestion', {suggestion_id: s.id}, function() {
3572 if (!OPTIONS.dismissed_suggestions) OPTIONS.dismissed_suggestions = [];
3573 OPTIONS.dismissed_suggestions.push(s.id);
3574 card.slideUp(300, function() { card.remove(); __renderContextSuggestions(container); });
3575 });
3576 }).appendTo(btnWrap);
3577
3578 card.append(btnWrap);
3579 wrap.append(card);
3580 });
3581
3582 container.prepend(wrap);
3583 }
3584
3585 // ── End Context-Wizards ──
3586
3587 // ── Version Notices ("What's New") ──
3588
3589 function __showVersionNotices() {
3590 var seenVersion = _getOptions_getValByKey('versionNoticeSeen');
3591 var currentVersion = myAjax._plugin_version;
3592 if (seenVersion === currentVersion) return null;
3593
3594 var notices = myAjax._versionNotices;
3595 if (!notices || !Array.isArray(notices) || notices.length === 0) return null;
3596
3597 var typeColors = {
3598 'feature': {border: '#0071e3', bg: '#eef4ff', icon: '&#9733;', iconColor: '#0071e3'},
3599 'info': {border: '#6b7280', bg: '#f9fafb', icon: '&#8505;', iconColor: '#6b7280'},
3600 'warning': {border: '#d97706', bg: '#fffbeb', icon: '&#9888;', iconColor: '#d97706'}
3601 };
3602
3603 var wrap = $('<div class="et-version-notices"/>');
3604
3605 if (!$('#et-version-notices-styles').length) {
3606 $('<style id="et-version-notices-styles"/>').text(
3607 '.et-version-notices{margin:15px 0 20px 0;}' +
3608 '.et-vn-card{background:#fff;border:1px solid #c3c4c7;padding:16px 20px;margin-bottom:10px;border-radius:6px;box-shadow:0 1px 1px rgba(0,0,0,.04);}' +
3609 '.et-vn-body{display:flex;align-items:flex-start;gap:12px;}' +
3610 '.et-vn-icon{font-size:20px;flex-shrink:0;margin-top:1px;}' +
3611 '.et-vn-text{flex:1;}' +
3612 '.et-vn-title{font-size:14px;font-weight:600;color:#1d2327;margin-bottom:3px;}' +
3613 '.et-vn-msg{font-size:13px;color:#50575e;line-height:1.5;}' +
3614 '.et-vn-link{font-size:13px;margin-left:4px;}' +
3615 '.et-vn-dismiss{margin-top:12px;text-align:right;}' +
3616 '.et-vn-dismiss-btn{display:inline-block;padding:5px 14px;border:1px solid #c3c4c7;border-radius:5px;background:#f6f7f7;color:#1d2327!important;text-decoration:none!important;font-size:13px;line-height:1.6;cursor:pointer;}' +
3617 '.et-vn-dismiss-btn:hover{background:#fff;border-color:#8c8f94;}'
3618 ).appendTo('head');
3619 }
3620
3621 notices.forEach(function(n) {
3622 var colors = typeColors[n.type] || typeColors.info;
3623 var card = $('<div class="et-vn-card"/>').css('border-left', '4px solid ' + colors.border).css('background', colors.bg);
3624 var body = $('<div class="et-vn-body"/>');
3625 $('<span class="et-vn-icon"/>').html(colors.icon).css('color', colors.iconColor).appendTo(body);
3626 var textWrap = $('<div class="et-vn-text"/>');
3627 $('<div class="et-vn-title"/>').text(n.title).appendTo(textWrap);
3628 var msgEl = $('<div class="et-vn-msg"/>').text(n.message);
3629 if (n.link && n.linkText) {
3630 msgEl.append(' ');
3631 $('<a class="et-vn-link"/>').attr('href', n.link).attr('target', '_blank').text(n.linkText + '').appendTo(msgEl);
3632 }
3633 textWrap.append(msgEl);
3634 body.append(textWrap);
3635 card.append(body);
3636 wrap.append(card);
3637 });
3638
3639 var dismissRow = $('<div class="et-vn-dismiss"/>');
3640 $('<button class="et-vn-dismiss-btn"/>')
3641 .text(__("Dismiss", 'event-tickets-with-ticket-scanner'))
3642 .on('click', function() {
3643 _saveOptionValue('versionNoticeSeen', currentVersion, function() {
3644 wrap.slideUp(300, function() { wrap.remove(); });
3645 });
3646 }).appendTo(dismissRow);
3647 wrap.append(dismissRow);
3648
3649 return wrap;
3650 }
3651
3652 var _firstStepsBox = null;
3653
3654 function __showFirstSteps() {
3655 if (!_getOptions_isActivatedByKey("displayFirstStepsHelp")) return null;
3656
3657 let hasTickets = OPTIONS.infos && OPTIONS.infos.ticket && OPTIONS.infos.ticket.counter > 0;
3658 let hasRedeemed = OPTIONS.infos && OPTIONS.infos.ticket && OPTIONS.infos.ticket.redeemed_count > 0;
3659 // DATA_LISTS may not be loaded yet — steps update later via __updateFirstStepsProgress()
3660 let hasLists = DATA_LISTS && DATA_LISTS.length > 0;
3661 let productsUrl = myAjax.url.replace('admin-ajax.php', 'edit.php?post_type=product');
3662 let scannerUrl = myAjax.ticket_url + 'scanner/';
3663
3664 let steps = [
3665 {key: 'list', done: hasLists, label: __('Create a ticket list', 'event-tickets-with-ticket-scanner'), desc: __('Organize your tickets in lists for different events or purposes.', 'event-tickets-with-ticket-scanner'), actionLabel: _x('View lists', 'label', 'event-tickets-with-ticket-scanner'), actionType: 'scroll'},
3666 {key: 'product', done: hasTickets, label: __('Assign list to a WooCommerce product', 'event-tickets-with-ticket-scanner'), desc: __('Open a product, go to the Event Ticket tab, and enable ticketing.', 'event-tickets-with-ticket-scanner'), actionLabel: _x('Go to products', 'label', 'event-tickets-with-ticket-scanner'), actionType: 'link', actionUrl: productsUrl},
3667 {key: 'order', done: hasTickets, label: __('Process a test order', 'event-tickets-with-ticket-scanner'), desc: __('Place an order and complete it to generate tickets.', 'event-tickets-with-ticket-scanner'), actionLabel: '', actionType: 'none'},
3668 {key: 'scan', done: hasRedeemed, label: __('Scan a ticket at the entrance', 'event-tickets-with-ticket-scanner'), desc: __('Use the browser-based QR scanner to redeem tickets.', 'event-tickets-with-ticket-scanner'), actionLabel: _x('Open Scanner', 'label', 'event-tickets-with-ticket-scanner'), actionType: 'link', actionUrl: scannerUrl}
3669 ];
3670
3671 let doneCount = steps.filter(s => s.done).length;
3672
3673 // --- Build card ---
3674 let card = $('<div class="saso-first-steps-card"/>');
3675
3676 // Header with progress
3677 let header = $('<div class="saso-first-steps-header"/>').appendTo(card);
3678 $('<h3/>').html(_x('Getting Started', 'title', 'event-tickets-with-ticket-scanner')).appendTo(header);
3679 let progressWrap = $('<div class="saso-first-steps-progress"/>').appendTo(header);
3680 let barOuter = $('<div class="saso-first-steps-bar-outer"/>').appendTo(progressWrap);
3681 let barInner = $('<div class="saso-first-steps-bar-inner"/>').css('width', (doneCount / steps.length * 100) + '%').appendTo(barOuter);
3682 let progressLabel = $('<span class="saso-first-steps-progress-label"/>').text(doneCount + '/' + steps.length).appendTo(progressWrap);
3683
3684 // Steps list
3685 let stepsList = $('<div class="saso-first-steps-list"/>').appendTo(card);
3686 steps.forEach((step, i) => {
3687 let row = $('<div class="saso-first-steps-step' + (step.done ? ' done' : '') + '" data-step="' + step.key + '"/>');
3688 let icon = $('<span class="saso-first-steps-icon"/>').html(step.done ? '&#10003;' : (i + 1)).appendTo(row);
3689 let content = $('<div class="saso-first-steps-content"/>').appendTo(row);
3690 $('<strong/>').text(step.label).appendTo(content);
3691 $('<div class="saso-first-steps-desc"/>').text(step.desc).appendTo(content);
3692 if (step.actionLabel && !step.done) {
3693 let btn = $('<button class="button button-small"/>').text(step.actionLabel).appendTo(content);
3694 if (step.actionType === 'scroll') {
3695 btn.on('click', () => {
3696 let target = $('#event-tickets-with-ticket-scanner-list-of-tickets');
3697 if (target.length) $('html,body').animate({scrollTop: target.offset().top - 50}, 400);
3698 });
3699 } else if (step.actionType === 'link') {
3700 btn.on('click', () => window.open(step.actionUrl, '_blank'));
3701 }
3702 }
3703 row.appendTo(stepsList);
3704 });
3705
3706 // Videos
3707 let videosWrap = $('<div class="saso-first-steps-videos"/>').appendTo(card);
3708 $(getUseFulVideosHTML()).appendTo(videosWrap);
3709
3710 // Footer with dismiss
3711 let footer = $('<div class="saso-first-steps-footer"/>').appendTo(card);
3712 $('<p/>').html(__('If you need help, please contact us via email. The information is in "Support Info" area - button above.', 'event-tickets-with-ticket-scanner')).appendTo(footer);
3713 let btn_dont_show = $('<button class="button button-secondary button-small"/>').html(_x("Don't show this again", 'label', 'event-tickets-with-ticket-scanner')).appendTo(footer);
3714 btn_dont_show.on("click", function(){
3715 _saveOptionValue("displayFirstStepsHelp", "0", ()=>{
3716 card.slideUp(300, function(){ card.remove(); _firstStepsBox = null; });
3717 });
3718 });
3719
3720 // Inject styles once
3721 if (!$('#saso-first-steps-styles').length) {
3722 $('<style id="saso-first-steps-styles"/>').text(
3723 '.saso-first-steps-card{background:#fff;border:1px solid #c3c4c7;border-left:4px solid #2271b1;padding:20px 24px;margin:20px 0;border-radius:4px;box-shadow:0 1px 1px rgba(0,0,0,.04);}' +
3724 '.saso-first-steps-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;}' +
3725 '.saso-first-steps-header h3{margin:0;font-size:16px;color:#1d2327;}' +
3726 '.saso-first-steps-progress{display:flex;align-items:center;gap:10px;}' +
3727 '.saso-first-steps-bar-outer{width:120px;height:8px;background:#dcdcde;border-radius:4px;overflow:hidden;}' +
3728 '.saso-first-steps-bar-inner{height:100%;background:#2271b1;border-radius:4px;transition:width .4s ease;}' +
3729 '.saso-first-steps-progress-label{font-size:13px;color:#646970;font-weight:500;}' +
3730 '.saso-first-steps-list{display:flex;flex-direction:column;gap:2px;}' +
3731 '.saso-first-steps-step{display:flex;align-items:flex-start;gap:14px;padding:12px 14px;border-radius:4px;transition:background .15s;}' +
3732 '.saso-first-steps-step:hover{background:#f6f7f7;}' +
3733 '.saso-first-steps-icon{flex-shrink:0;width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;background:#dcdcde;color:#50575e;}' +
3734 '.saso-first-steps-step.done .saso-first-steps-icon{background:#00a32a;color:#fff;}' +
3735 '.saso-first-steps-content{flex:1;min-width:0;}' +
3736 '.saso-first-steps-content strong{display:block;font-size:14px;color:#1d2327;margin-bottom:2px;}' +
3737 '.saso-first-steps-step.done .saso-first-steps-content strong{color:#646970;text-decoration:line-through;}' +
3738 '.saso-first-steps-desc{font-size:13px;color:#646970;margin-bottom:6px;}' +
3739 '.saso-first-steps-content .button{margin-top:2px;}' +
3740 '.saso-first-steps-videos{margin-top:14px;padding-top:14px;border-top:1px solid #dcdcde;}' +
3741 '.saso-first-steps-videos h3{font-size:14px;margin:0 0 6px;}' +
3742 '.saso-first-steps-videos ul{margin:0 0 0 18px;}' +
3743 '.saso-first-steps-videos li{margin-bottom:4px;}' +
3744 '.saso-first-steps-footer{margin-top:14px;padding-top:14px;border-top:1px solid #dcdcde;display:flex;align-items:center;justify-content:space-between;gap:12px;}' +
3745 '.saso-first-steps-footer p{margin:0;font-size:13px;color:#646970;}'
3746 ).appendTo('head');
3747 }
3748
3749 _firstStepsBox = card;
3750 return card;
3751 }
3752
3753 function __updateFirstStepsProgress() {
3754 if (!_firstStepsBox) return;
3755 let hasLists = DATA_LISTS && DATA_LISTS.length > 0;
3756 let hasTickets = OPTIONS.infos && OPTIONS.infos.ticket && OPTIONS.infos.ticket.counter > 0;
3757 let hasRedeemed = OPTIONS.infos && OPTIONS.infos.ticket && OPTIONS.infos.ticket.redeemed_count > 0;
3758 let states = {list: hasLists, product: hasTickets, order: hasTickets, scan: hasRedeemed};
3759 let doneCount = 0;
3760 _firstStepsBox.find('.saso-first-steps-step').each(function(){
3761 let key = $(this).data('step');
3762 let done = states[key] || false;
3763 if (done) doneCount++;
3764 $(this).toggleClass('done', done);
3765 $(this).find('.saso-first-steps-icon').html(done ? '&#10003;' : (Object.keys(states).indexOf(key) + 1));
3766 if (done) $(this).find('.button').hide();
3767 });
3768 _firstStepsBox.find('.saso-first-steps-bar-inner').css('width', (doneCount / 4 * 100) + '%');
3769 _firstStepsBox.find('.saso-first-steps-progress-label').text(doneCount + '/4');
3770 }
3771
3772 // ============================================================
3773 // Setup Wizard (#187 Phase 2)
3774 // ============================================================
3775
3776 var _wizardPresetQuestions = {
3777 'event': [
3778 {key: 'wcTicketDontAllowRedeemTicketBeforeStart', label: __('Lock ticket redemption until event starts?', 'event-tickets-with-ticket-scanner'), preset: 1},
3779 {key: 'ticketScannerScanAndRedeemImmediately', label: __('Auto-redeem when scanned?', 'event-tickets-with-ticket-scanner'), preset: 1},
3780 {key: 'wcTicketDisplayOrderTicketsViewLinkOnMail', label: __('Show "Open Tickets" link in email? All QR codes on one page — ideal for groups.', 'event-tickets-with-ticket-scanner'), preset: 1},
3781 {key: 'wcTicketSetOrderToCompleteIfAllOrderItemsAreTickets', label: __('Auto-complete orders when all items are tickets? Tickets are generated immediately.', 'event-tickets-with-ticket-scanner'), preset: 1},
3782 {key: 'walletVollstartEnable', label: __('Enable Vollstart Wallet? Customers can collect tickets in the free wallet app.', 'event-tickets-with-ticket-scanner'), preset: 1}
3783 ],
3784 'daypass': [
3785 {key: 'wcTicketAllowRedeemTicketAfterEnd', label: __('Allow redemption after closing time?', 'event-tickets-with-ticket-scanner'), preset: 1},
3786 {key: 'ticketScannerScanAndRedeemImmediately', label: __('Auto-redeem when scanned?', 'event-tickets-with-ticket-scanner'), preset: 1},
3787 {key: 'wcTicketDisplayOrderTicketsViewLinkOnMail', label: __('Show "Open Tickets" link in email? All QR codes on one page — ideal for families.', 'event-tickets-with-ticket-scanner'), preset: 1},
3788 {key: 'wcTicketSetOrderToCompleteIfAllOrderItemsAreTickets', label: __('Auto-complete orders when all items are tickets? Tickets are generated immediately.', 'event-tickets-with-ticket-scanner'), preset: 1},
3789 {key: 'walletVollstartEnable', label: __('Enable Vollstart Wallet? Customers can collect tickets in the free wallet app.', 'event-tickets-with-ticket-scanner'), preset: 1}
3790 ],
3791 'membership': [
3792 {key: 'wcTicketUserProfileDisplayRedeemAmount', label: __('Show redemption counter to customer? E.g. "15 of 30 visits used"', 'event-tickets-with-ticket-scanner'), preset: 1},
3793 {key: 'wcTicketShowRedeemBtnOnTicket', label: __('Show self-redeem button on ticket page? For self-service access.', 'event-tickets-with-ticket-scanner'), preset: 1},
3794 {key: 'ticketScannerScanAndRedeemImmediately', label: __('Auto-redeem when scanned? Disable to verify identity first.', 'event-tickets-with-ticket-scanner'), preset: 0},
3795 {key: 'walletVollstartEnable', label: __('Enable Vollstart Wallet? Customers can collect tickets in the free wallet app.', 'event-tickets-with-ticket-scanner'), preset: 1}
3796 ],
3797 'voucher': [
3798 {key: 'ticketScannerScanAndRedeemImmediately', label: __('Auto-redeem when scanned?', 'event-tickets-with-ticket-scanner'), preset: 1},
3799 {key: 'wcTicketShowRedeemBtnOnTicket', label: __('Show self-redeem button? Customer redeems the voucher themselves.', 'event-tickets-with-ticket-scanner'), preset: 1},
3800 {key: 'walletVollstartEnable', label: __('Enable Vollstart Wallet? Customers can collect tickets in the free wallet app.', 'event-tickets-with-ticket-scanner'), preset: 1}
3801 ]
3802 };
3803
3804 var _wizardUseCases = [
3805 {key: 'event', icon: '&#127915;', label: __('Event tickets', 'event-tickets-with-ticket-scanner'), desc: __('Concerts, shows, festivals', 'event-tickets-with-ticket-scanner')},
3806 {key: 'daypass', icon: '&#127965;', label: __('Day passes', 'event-tickets-with-ticket-scanner'), desc: __('Theme park, zoo, spa', 'event-tickets-with-ticket-scanner')},
3807 {key: 'membership', icon: '&#127183;', label: __('Memberships / Season passes', 'event-tickets-with-ticket-scanner'), desc: __('Recurring access', 'event-tickets-with-ticket-scanner')},
3808 {key: 'voucher', icon: '&#127873;', label: __('Vouchers / Simple codes', 'event-tickets-with-ticket-scanner'), desc: __('Gift cards, promo codes', 'event-tickets-with-ticket-scanner')}
3809 ];
3810
3811 // ── Premium Update Check after serial key entry ──────────────────
3812 function __checkPremiumUpdateAfterSerial(serialValue) {
3813 if (!serialValue || serialValue.trim() === '') return;
3814 _makePost('checkPremiumUpdate', {}, function(r) {
3815 if (r.hasUpdate) {
3816 __showPremiumUpdateDialog(r);
3817 } else {
3818 __showPremiumReleaseNotesHint();
3819 }
3820 });
3821 }
3822
3823 function __showPremiumUpdateDialog(updateInfo) {
3824 let dlg = $('<div/>').html(
3825 '<p>' + __('A new premium version is available!', 'event-tickets-with-ticket-scanner') + '</p>' +
3826 '<p>' + sprintf(__('Version %s is ready to install.', 'event-tickets-with-ticket-scanner'), '<b>' + updateInfo.newVersion + '</b>') + '</p>' +
3827 '<p>' + __('Click the button below to update your premium plugin now.', 'event-tickets-with-ticket-scanner') + '</p>'
3828 );
3829 dlg.dialog({
3830 title: __('Premium Plugin Update', 'event-tickets-with-ticket-scanner'),
3831 modal: true,
3832 width: 450,
3833 buttons: [{
3834 text: __('Update Now', 'event-tickets-with-ticket-scanner'),
3835 class: 'button button-primary',
3836 click: function() { window.location.href = updateInfo.updateUrl; }
3837 }, {
3838 text: __('Later', 'event-tickets-with-ticket-scanner'),
3839 class: 'button',
3840 click: function() { $(this).dialog('close'); }
3841 }]
3842 });
3843 }
3844
3845 function __showPremiumReleaseNotesHint() {
3846 let changelogUrl = 'https://vollstart.com/plugins/event-tickets-with-ticket-scanner-premium/changelog.json';
3847 let today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
3848 fetch(changelogUrl + '?t=' + today)
3849 .then(function(r) { return r.json(); })
3850 .then(function(data) {
3851 if (!data.versions || data.versions.length === 0) return;
3852 let latest = data.versions[0];
3853 let html = '<p>' + sprintf(
3854 __('Premium version %s is available with new features:', 'event-tickets-with-ticket-scanner'),
3855 '<b>' + latest.version + '</b>'
3856 ) + '</p><ul>';
3857 for (let i = 0; i < latest.changes.length; i++) {
3858 html += '<li>' + latest.changes[i] + '</li>';
3859 }
3860 html += '</ul>';
3861 html += '<p><a href="https://vollstart.com/event-tickets-with-woocommerce/" target="_blank" class="button">' +
3862 __('Learn more', 'event-tickets-with-ticket-scanner') + '</a></p>';
3863 let dlg = $('<div/>').html(html);
3864 dlg.dialog({
3865 title: __('New Premium Features Available', 'event-tickets-with-ticket-scanner'),
3866 modal: false,
3867 width: 500,
3868 buttons: [{
3869 text: __('Close', 'event-tickets-with-ticket-scanner'),
3870 class: 'button',
3871 click: function() { $(this).dialog('close'); }
3872 }]
3873 });
3874 })
3875 .catch(function() { /* silently fail */ });
3876 }
3877
3878 function __showSetupWizard(force) {
3879 if (!force) {
3880 let wizardVal = _getOptions_getValByKey('wizardCompleted');
3881 if (wizardVal && wizardVal !== '') return;
3882 }
3883
3884 // Inject wizard styles once
3885 if (!$('#saso-wizard-styles').length) {
3886 $('<style id="saso-wizard-styles"/>').text(
3887 '.saso-wizard-dialog .ui-dialog-titlebar{background:#2271b1;color:#fff;border:none;border-radius:4px 4px 0 0;padding:12px 16px;}' +
3888 '.saso-wizard-dialog .ui-dialog-titlebar-close{display:none;}' +
3889 '.saso-wizard-dialog .ui-dialog-buttonpane{border-top:1px solid #dcdcde;padding:12px 16px;}' +
3890 '.saso-wizard-dialog .ui-dialog-buttonpane button{margin-left:8px;}' +
3891 '.saso-wizard{padding:8px 0;min-height:250px;}' +
3892 '.saso-wizard-steps{display:flex;justify-content:center;gap:8px;margin-bottom:20px;}' +
3893 '.saso-wizard-step-dot{width:10px;height:10px;border-radius:50%;background:#dcdcde;transition:background .2s;}' +
3894 '.saso-wizard-step-dot.active{background:#2271b1;}' +
3895 '.saso-wizard-step-dot.done{background:#00a32a;}' +
3896 '.saso-wizard-welcome{text-align:center;padding:20px 0;}' +
3897 '.saso-wizard-welcome h2{font-size:20px;margin:0 0 10px;color:#1d2327;}' +
3898 '.saso-wizard-welcome p{font-size:14px;color:#646970;margin:0 0 6px;}' +
3899 '.saso-wizard-usecases{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:0 4px;}' +
3900 '.saso-wizard-usecase{border:2px solid #dcdcde;border-radius:8px;padding:16px;cursor:pointer;transition:border-color .15s,background .15s;text-align:center;}' +
3901 '.saso-wizard-usecase:hover{border-color:#2271b1;background:#f0f6fc;}' +
3902 '.saso-wizard-usecase.selected{border-color:#2271b1;background:#e7f0f9;}' +
3903 '.saso-wizard-usecase-icon{font-size:28px;display:block;margin-bottom:6px;}' +
3904 '.saso-wizard-usecase-label{font-size:14px;font-weight:600;color:#1d2327;display:block;}' +
3905 '.saso-wizard-usecase-desc{font-size:12px;color:#646970;display:block;margin-top:2px;}' +
3906 '.saso-wizard-questions{display:flex;flex-direction:column;gap:14px;padding:0 4px;}' +
3907 '.saso-wizard-question{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border:1px solid #dcdcde;border-radius:6px;background:#fff;}' +
3908 '.saso-wizard-question-label{font-size:14px;color:#1d2327;flex:1;padding-right:12px;}' +
3909 '.saso-wizard-toggle{position:relative;width:44px;height:24px;flex-shrink:0;}' +
3910 '.saso-wizard-toggle input{opacity:0;width:0;height:0;}' +
3911 '.saso-wizard-toggle .slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#ccc;border-radius:24px;transition:.2s;}' +
3912 '.saso-wizard-toggle .slider:before{content:"";position:absolute;height:18px;width:18px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.2s;}' +
3913 '.saso-wizard-toggle input:checked + .slider{background:#2271b1;}' +
3914 '.saso-wizard-toggle input:checked + .slider:before{transform:translateX(20px);}' +
3915 '.saso-wizard-done{text-align:center;padding:20px 0;}' +
3916 '.saso-wizard-done-icon{font-size:48px;color:#00a32a;display:block;margin-bottom:10px;}' +
3917 '.saso-wizard-done h2{font-size:20px;margin:0 0 10px;color:#1d2327;}' +
3918 '.saso-wizard-done p{font-size:14px;color:#646970;margin:0 0 6px;}' +
3919 '.saso-wizard-done-steps{text-align:left;margin:16px auto;max-width:320px;}' +
3920 '.saso-wizard-done-steps li{font-size:13px;color:#1d2327;margin-bottom:4px;}' +
3921 '.saso-wizard-tip{margin-top:14px;padding:12px 14px;background:#f0f6fc;border:1px solid #c3c4c7;border-left:3px solid #2271b1;border-radius:4px;font-size:13px;color:#1d2327;}' +
3922 '.saso-wizard-tip strong{display:block;margin-bottom:4px;}' +
3923 '.saso-wizard-tip span{color:#646970;}'
3924 ).appendTo('head');
3925 }
3926
3927 var currentStep = 1;
3928 var selectedPreset = '';
3929 var overrides = {};
3930
3931 var dlg = $('<div class="saso-wizard"/>');
3932
3933 function renderStepDots() {
3934 var dots = $('<div class="saso-wizard-steps"/>');
3935 for (var i = 1; i <= 4; i++) {
3936 var cls = 'saso-wizard-step-dot';
3937 if (i < currentStep) cls += ' done';
3938 if (i === currentStep) cls += ' active';
3939 $('<span/>').addClass(cls).appendTo(dots);
3940 }
3941 return dots;
3942 }
3943
3944 function renderStep() {
3945 dlg.empty();
3946 dlg.append(renderStepDots());
3947
3948 if (currentStep === 1) {
3949 var welcome = $('<div class="saso-wizard-welcome"/>');
3950 $('<h2/>').text(__('Welcome to Event Tickets!', 'event-tickets-with-ticket-scanner')).appendTo(welcome);
3951 $('<p/>').text(__("Let's configure your ticketing system.", 'event-tickets-with-ticket-scanner')).appendTo(welcome);
3952 $('<p/>').text(__('This takes less than 2 minutes.', 'event-tickets-with-ticket-scanner')).appendTo(welcome);
3953 dlg.append(welcome);
3954
3955 dlg.dialog('option', 'buttons', [
3956 {text: __("Skip, I'll configure manually", 'event-tickets-with-ticket-scanner'), class: 'button', click: function() { skipWizard(); }},
3957 {text: __('Start Setup', 'event-tickets-with-ticket-scanner'), class: 'button button-primary', click: function() { currentStep = 2; renderStep(); }}
3958 ]);
3959 }
3960 else if (currentStep === 2) {
3961 var wrap = $('<div class="saso-wizard-usecases"/>');
3962 _wizardUseCases.forEach(function(uc) {
3963 var card = $('<div class="saso-wizard-usecase"/>').attr('data-preset', uc.key);
3964 if (uc.key === selectedPreset) card.addClass('selected');
3965 $('<span class="saso-wizard-usecase-icon"/>').html(uc.icon).appendTo(card);
3966 $('<span class="saso-wizard-usecase-label"/>').text(uc.label).appendTo(card);
3967 $('<span class="saso-wizard-usecase-desc"/>').text(uc.desc).appendTo(card);
3968 card.on('click', function() {
3969 selectedPreset = uc.key;
3970 overrides = {};
3971 wrap.find('.saso-wizard-usecase').removeClass('selected');
3972 $(this).addClass('selected');
3973 updateButtons();
3974 });
3975 wrap.append(card);
3976 });
3977 dlg.append(wrap);
3978
3979 function updateButtons() {
3980 dlg.dialog('option', 'buttons', [
3981 {text: __('Back', 'event-tickets-with-ticket-scanner'), class: 'button', click: function() { currentStep = 1; renderStep(); }},
3982 {text: __('Next', 'event-tickets-with-ticket-scanner'), class: 'button button-primary', disabled: !selectedPreset, click: function() {
3983 if (!selectedPreset) return;
3984 currentStep = 3;
3985 renderStep();
3986 }}
3987 ]);
3988 }
3989 updateButtons();
3990 }
3991 else if (currentStep === 3) {
3992 var questions = _wizardPresetQuestions[selectedPreset] || [];
3993 var qWrap = $('<div class="saso-wizard-questions"/>');
3994
3995 var ucLabel = '';
3996 _wizardUseCases.forEach(function(uc) { if (uc.key === selectedPreset) ucLabel = uc.label; });
3997 $('<p/>').css({fontSize:'14px',color:'#1d2327',margin:'0 4px 10px',fontWeight:'600'}).text(ucLabel).appendTo(qWrap);
3998
3999 questions.forEach(function(q) {
4000 var row = $('<div class="saso-wizard-question"/>');
4001 $('<span class="saso-wizard-question-label"/>').text(q.label).appendTo(row);
4002 var toggle = $('<label class="saso-wizard-toggle"/>');
4003 var val = typeof overrides[q.key] !== 'undefined' ? overrides[q.key] : q.preset;
4004 var input = $('<input type="checkbox"/>').prop('checked', !!val);
4005 input.on('change', function() {
4006 overrides[q.key] = $(this).prop('checked') ? 1 : 0;
4007 });
4008 toggle.append(input);
4009 toggle.append($('<span class="slider"/>'));
4010 row.append(toggle);
4011 qWrap.append(row);
4012 });
4013
4014 if (!isPremium()) {
4015 var tip = $('<div class="saso-wizard-tip"/>');
4016 $('<strong/>').text('&#9889; ' + __('Tip: PDF attachment to email', 'event-tickets-with-ticket-scanner')).appendTo(tip);
4017 $('<span/>').text(__('With the Premium version you can attach PDF tickets directly to the order email — as individual files or merged into one PDF.', 'event-tickets-with-ticket-scanner')).appendTo(tip);
4018 qWrap.append(tip);
4019 } else {
4020 var premiumQuestions = [];
4021 if (selectedPreset === 'event' || selectedPreset === 'daypass') {
4022 premiumQuestions.push({key: 'wcTicketAttachTicketToMailAsOnePDF', label: __('Attach all tickets as one PDF to the order email?', 'event-tickets-with-ticket-scanner'), preset: 1});
4023 } else {
4024 premiumQuestions.push({key: 'wcTicketAttachTicketToMail', label: __('Attach PDF ticket to the order email?', 'event-tickets-with-ticket-scanner'), preset: 1});
4025 }
4026 if (premiumQuestions.length > 0) {
4027 var premLabel = $('<p/>').css({fontSize:'13px',color:'#2271b1',margin:'10px 4px 6px',fontWeight:'600'}).text(__('Premium', 'event-tickets-with-ticket-scanner'));
4028 qWrap.append(premLabel);
4029 premiumQuestions.forEach(function(q) {
4030 var row = $('<div class="saso-wizard-question"/>');
4031 $('<span class="saso-wizard-question-label"/>').text(q.label).appendTo(row);
4032 var toggle = $('<label class="saso-wizard-toggle"/>');
4033 var val = typeof overrides[q.key] !== 'undefined' ? overrides[q.key] : q.preset;
4034 var input = $('<input type="checkbox"/>').prop('checked', !!val);
4035 input.on('change', function() {
4036 overrides[q.key] = $(this).prop('checked') ? 1 : 0;
4037 });
4038 toggle.append(input);
4039 toggle.append($('<span class="slider"/>'));
4040 row.append(toggle);
4041 qWrap.append(row);
4042 });
4043 }
4044 }
4045
4046 dlg.append(qWrap);
4047
4048 dlg.dialog('option', 'buttons', [
4049 {text: __('Back', 'event-tickets-with-ticket-scanner'), class: 'button', click: function() { currentStep = 2; renderStep(); }},
4050 {text: __('Apply & Finish', 'event-tickets-with-ticket-scanner'), class: 'button button-primary', click: function() { applyPreset(); }}
4051 ]);
4052 }
4053 else if (currentStep === 4) {
4054 var done = $('<div class="saso-wizard-done"/>');
4055 $('<span class="saso-wizard-done-icon"/>').html('&#10003;').appendTo(done);
4056 $('<h2/>').text(__("You're all set!", 'event-tickets-with-ticket-scanner')).appendTo(done);
4057
4058 var ucLabel2 = '';
4059 _wizardUseCases.forEach(function(uc) { if (uc.key === selectedPreset) ucLabel2 = uc.label; });
4060 $('<p/>').text(sprintf(__('Your settings have been configured for %s.', 'event-tickets-with-ticket-scanner'), ucLabel2)).appendTo(done);
4061
4062 var nextSteps = $('<ol class="saso-wizard-done-steps"/>');
4063 $('<li/>').text(__('Create a ticket list', 'event-tickets-with-ticket-scanner')).appendTo(nextSteps);
4064 $('<li/>').text(__('Enable tickets on a WooCommerce product', 'event-tickets-with-ticket-scanner')).appendTo(nextSteps);
4065 $('<li/>').text(__('Place a test order', 'event-tickets-with-ticket-scanner')).appendTo(nextSteps);
4066 done.append(nextSteps);
4067 dlg.append(done);
4068
4069 var scannerUrl = myAjax.ticket_url + 'scanner/';
4070 dlg.dialog('option', 'buttons', [
4071 {text: __('Open Options', 'event-tickets-with-ticket-scanner'), class: 'button', click: function() {
4072 closeDialog(dlg);
4073 var settingsBtn = $('[data-action="settings"]');
4074 if (settingsBtn.length) settingsBtn.trigger('click');
4075 }},
4076 {text: __('Close', 'event-tickets-with-ticket-scanner'), class: 'button button-primary', click: function() { closeDialog(dlg); }}
4077 ]);
4078 }
4079 }
4080
4081 function skipWizard() {
4082 _saveOptionValue('wizardCompleted', myAjax._plugin_version, function() {
4083 closeDialog(dlg);
4084 });
4085 }
4086
4087 function applyPreset() {
4088 dlg.dialog('option', 'buttons', []);
4089 dlg.empty().html('<div style="text-align:center;padding:40px 0;">' + _getSpinnerHTML(__('Applying settings...', 'event-tickets-with-ticket-scanner')) + '</div>');
4090 _makePost('applyWizardPreset', {preset: selectedPreset, overrides: JSON.stringify(overrides)}, function() {
4091 currentStep = 4;
4092 renderStep();
4093 }, function(err) {
4094 currentStep = 3;
4095 renderStep();
4096 alert(__('Error applying settings. Please try again.', 'event-tickets-with-ticket-scanner'));
4097 });
4098 }
4099
4100 dlg.dialog({
4101 title: __('Setup Wizard', 'event-tickets-with-ticket-scanner'),
4102 modal: true,
4103 width: 550,
4104 minHeight: 350,
4105 dialogClass: 'saso-wizard-dialog',
4106 closeOnEscape: false,
4107 buttons: [],
4108 open: function() { renderStep(); }
4109 });
4110 }
4111
4112 // ── Premium Wizard (#233) ──────────────────────────────────────
4113
4114 function __showPremiumWizard(force) {
4115 if (!isPremium()) return;
4116 if (!force) {
4117 let val = _getOptions_getValByKey('premiumWizardCompleted');
4118 if (val && val !== '') return;
4119 // Don't show if setup wizard hasn't been completed yet (let that run first)
4120 let setupVal = _getOptions_getValByKey('wizardCompleted');
4121 if (!setupVal || setupVal === '') return;
4122 }
4123
4124 let dlg = $('<div class="saso-wizard"/>');
4125 let step = 1;
4126
4127 function renderStep() {
4128 dlg.empty();
4129 if (step === 1) {
4130 dlg.html(
4131 '<div style="text-align:center;margin:20px 0 10px;">' +
4132 '<span style="font-size:48px;">&#11088;</span>' +
4133 '<h2 style="margin:10px 0 5px;">' + __('Premium Activated!', 'event-tickets-with-ticket-scanner') + '</h2>' +
4134 '<p style="color:#666;">' + __('You now have access to these features:', 'event-tickets-with-ticket-scanner') + '</p>' +
4135 '</div>' +
4136 '<ul style="margin:0 0 15px 20px;line-height:1.8;">' +
4137 '<li>' + __('PDF ticket attachment in emails', 'event-tickets-with-ticket-scanner') + '</li>' +
4138 '<li>' + __('Ticket Designer (custom PDF templates)', 'event-tickets-with-ticket-scanner') + '</li>' +
4139 '<li>' + __('Badge printing', 'event-tickets-with-ticket-scanner') + '</li>' +
4140 '<li>' + __('Seating plans', 'event-tickets-with-ticket-scanner') + '</li>' +
4141 '</ul>' +
4142 '<div style="background:#f0f6fc;border:1px solid #c3d9ed;border-radius:6px;padding:12px;margin:10px 0;">' +
4143 '<strong>' + __('Recommended:', 'event-tickets-with-ticket-scanner') + '</strong> ' +
4144 __('Enable PDF ticket attachment in emails? Customers receive their tickets as PDF directly in the order confirmation email.', 'event-tickets-with-ticket-scanner') +
4145 '</div>'
4146 );
4147 dlg.dialog('option', 'buttons', [
4148 {
4149 text: __('Enable Recommended Settings', 'event-tickets-with-ticket-scanner'),
4150 class: 'button button-primary',
4151 click: function() {
4152 dlg.html('<div style="text-align:center;padding:40px;">' + _getSpinnerHTML() + '</div>');
4153 dlg.dialog('option', 'buttons', []);
4154 _makePost('applyPremiumDefaults', {}, function(r) {
4155 if (OPTIONS.mapKeys['premiumWizardCompleted']) OPTIONS.mapKeys['premiumWizardCompleted'].value = r._version || '1';
4156 step = 2;
4157 renderStep();
4158 });
4159 }
4160 },
4161 {
4162 text: _x('Skip', 'button', 'event-tickets-with-ticket-scanner'),
4163 class: 'button',
4164 click: function() {
4165 _saveOptionValue('premiumWizardCompleted', '1', function() {
4166 if (OPTIONS.mapKeys['premiumWizardCompleted']) OPTIONS.mapKeys['premiumWizardCompleted'].value = '1';
4167 dlg.dialog('close');
4168 dlg.dialog('destroy');
4169 dlg.remove();
4170 });
4171 }
4172 }
4173 ]);
4174 } else if (step === 2) {
4175 dlg.html(
4176 '<div style="text-align:center;margin:20px 0 10px;">' +
4177 '<span style="font-size:48px;">&#9989;</span>' +
4178 '<h2 style="margin:10px 0 5px;">' + __('Premium settings applied!', 'event-tickets-with-ticket-scanner') + '</h2>' +
4179 '</div>' +
4180 '<ul style="margin:0 0 15px 20px;line-height:1.8;">' +
4181 '<li>' + __('PDF attachment in emails: enabled', 'event-tickets-with-ticket-scanner') + '</li>' +
4182 '<li>' + __('Merge into one PDF: enabled', 'event-tickets-with-ticket-scanner') + '</li>' +
4183 '<li>' + __('Max attachments: 21', 'event-tickets-with-ticket-scanner') + '</li>' +
4184 '</ul>' +
4185 '<p style="color:#666;text-align:center;">' + __('You can change these anytime in Options.', 'event-tickets-with-ticket-scanner') + '</p>'
4186 );
4187 dlg.dialog('option', 'buttons', [
4188 {
4189 text: __('Close', 'event-tickets-with-ticket-scanner'),
4190 class: 'button button-primary',
4191 click: function() {
4192 dlg.dialog('close');
4193 dlg.dialog('destroy');
4194 dlg.remove();
4195 }
4196 }
4197 ]);
4198 }
4199 }
4200
4201 dlg.dialog({
4202 title: __('Premium Features', 'event-tickets-with-ticket-scanner'),
4203 modal: true,
4204 width: 480,
4205 minHeight: 280,
4206 dialogClass: 'saso-wizard-dialog',
4207 closeOnEscape: false,
4208 buttons: [],
4209 open: function() { renderStep(); }
4210 });
4211 }
4212
4213 class Layout {
4214 constructor(){
4215 DIV.addClass("sngmbh_container");
4216 this.div_liste = $('<div class="et-card" id="event-tickets-with-ticket-scanner-list-of-tickets"/>').html(_getSpinnerHTML());
4217 this.div_codes = $('<div class="et-card"/>').html(_getSpinnerHTML());
4218 this.div_spinner = $('<div class="et-spinner-overlay"/>').html(_getSpinnerHTML("loading"));
4219 $("body").append(this.div_spinner);
4220 }
4221 renderMainBody() {
4222 let versionNotices = __showVersionNotices();
4223 let infoBoxFirstSteps = __showFirstSteps();
4224 __showSetupWizard();
4225 __showPremiumWizard();
4226
4227 // display upgrade to premium link
4228 if (!isPremium()) {
4229 let btn_upgrade = $('<a/>')
4230 .html('<img src="'+myAjax._plugin_home_url+'/img/button_premium_icon.gif" alt="" class="event-tickets-with-ticket-scanner-upgrade-icon">' + _x('Upgrade', 'label', 'event-tickets-with-ticket-scanner'))
4231 .addClass("event-tickets-with-ticket-scanner-upgrade-btn")
4232 .attr("href", getPremiumProductURL())
4233 .attr("target", "_blank");
4234 $('body').find('#event-tickets-with-ticket-scanner-header-actions').html(btn_upgrade);
4235 }
4236
4237 let div_body = $('<div/>');
4238 div_body.append(
4239 $('<div class="event-tickets-with-ticket-scanner-topbar">')
4240 .html($('<div/>').addClass("event-tickets-with-ticket-scanner-topbar-left"))
4241 .append(_displaySettingAreaButton())
4242 );
4243 if (versionNotices) {
4244 div_body.append(versionNotices);
4245 }
4246 if (infoBoxFirstSteps) {
4247 div_body.append(infoBoxFirstSteps);
4248 }
4249 div_body.append(this.div_liste);
4250 div_body.append($('<h3/>').html(_x("Event Tickets", 'title', 'event-tickets-with-ticket-scanner')));
4251 div_body.append(this.div_codes);
4252 return div_body;
4253 }
4254 renderAddCodes() {
4255 DIV.html(_getSpinnerHTML());
4256 getDataLists(()=>{
4257 function __generateCodes() {
4258 // generate codes and
4259 let amount_codes = parseInt(input_amount_codes.val().trim(),10);
4260 if (isNaN(amount_codes) || amount_codes < 1) {
4261 input_amount_codes.select();
4262 return alert(_x("Enter an amount of how many ticket numbers you need", 'title', 'event-tickets-with-ticket-scanner'));
4263 }
4264 if (amount_codes > _maxCodes) {
4265 input_amount_codes.val(_maxCodes);
4266 amount_codes = _maxCodes;
4267
4268 }
4269 let uniq = {};
4270 let versuche = 0;
4271 if (serialCodeFormatterForm.isTypeNumbers()) { // numbers
4272 for(let a=0; a < amount_codes; a++) {
4273 let code = serialCodeFormatterForm.generateSerialCode( a );
4274 if (typeof uniq[code] !== "undefined") {
4275 continue;
4276 }
4277 uniq[code] = true;
4278 }
4279 versuche = amount_codes;
4280 } else {
4281 // erstmal kein check ob mit dem alphabet und die geforderte Menge an letters, unique codes erstellt werden können
4282 let counter = 0;
4283 let versuche_max = amount_codes * 1.5;
4284 while(counter < amount_codes && versuche < versuche_max) {
4285 versuche++;
4286 let code = serialCodeFormatterForm.generateSerialCode();
4287 if (typeof uniq[code] !== "undefined") {
4288 continue;
4289 }
4290 uniq[code] = true;
4291 counter++;
4292 }
4293 }
4294 return [Object.keys(uniq), versuche];
4295 } // __generateCodes
4296
4297 let div = $('<div>').append(getBackButtonDiv());
4298 // eingabe generator options
4299 let div_generator = $('<div class="et-card"/>').html('<h3>'+_x('1. Ticket number generator (optional step)', 'title', 'event-tickets-with-ticket-scanner')+'</h3>').appendTo(div);
4300 div_generator.append($('<p>').html(__("You can generate ticket numbers.", 'event-tickets-with-ticket-scanner')));
4301 if (isPremium()) div_generator.append('<p>'+__('Up 100.000 tickets generation per run. The limit is to prevent performance issues.', 'event-tickets-with-ticket-scanner')+'<br>'+__('You can repeat the "store tickets" operations as often as needed.', 'event-tickets-with-ticket-scanner')+'</p>');
4302 // anzahl codes
4303 let div_amount_codes = _createDivInput(_x('Enter amount of needed ticket numbers', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div_generator);
4304 let _maxCodes = myAjax._max.codes;
4305 if (!isPremium()) div_amount_codes.append(sprintf(/* translators: 1: amount of possible codes 2: premium info */__('%1$d max. %2$s up to 100.000 for each run', 'event-tickets-with-ticket-scanner'), _maxCodes, getLabelPremiumOnly())+'<br>');
4306 let input_amount_codes = $('<input type="number" required value="100" min="1" max="'+_maxCodes+'">').appendTo(div_amount_codes);
4307
4308 // predefine elements
4309 let serialCodeFormatterForm = _form_fields_serial_format(div_generator);
4310 serialCodeFormatterForm.render();
4311
4312 let elem_clean_codebox = $('<input checked type="checkbox" />');
4313 $('<div/>').css({"margin-bottom": "15px","margin-right": "15px"})
4314 .html(elem_clean_codebox)
4315 .append(_x('Clear the ticket numbers list textarea field below to add fill in the new generated ticket numbers', 'label', 'event-tickets-with-ticket-scanner'))
4316 .appendTo(div_generator);
4317
4318 let elem_create_cvv = $('<input type="checkbox" />');
4319 $('<div/>').css({"margin-bottom": "15px","margin-right": "15px"})
4320 .html(elem_create_cvv)
4321 .append(_x('Generate Code Verification Value (CVV) for each ticket number', 'label', 'event-tickets-with-ticket-scanner'))
4322 .appendTo(div_generator);
4323
4324 // button generate
4325 div_generator.append($('<button/>').addClass("button-secondary").html(_x('Generate ticket numbers', 'label', 'event-tickets-with-ticket-scanner')).on("click", function(){
4326 let time_start = performance.now();
4327 btn_store_codes.prop("disabled", false);
4328 input_textarea.prop("disabled", false);
4329 if (elem_clean_codebox[0].checked) {
4330 input_textarea.html("");
4331 }
4332 input_textarea.prop("disabled", true);
4333 div_textarea_info.css("padding-bottom", "50px").html(_getSpinnerHTML());
4334 setTimeout(function(){
4335 let r = __generateCodes();
4336 let codes = r[0];
4337 let secs = ((performance.now() - time_start) / 1000)+"";
4338 if (elem_create_cvv[0].checked) {
4339 codes = codes.map(v=>{
4340 return v += ';'+(Math.floor(Math.random() * 10000) + 10000).toString().substring(1);
4341 });
4342 }
4343 input_textarea.append(codes.join("\n")).append("\n");
4344 input_textarea.prop("disabled", false);
4345 div_textarea_info.html(sprintf(/* translators: 1: amount of created tickets 2: seconds 3: amount of runs */__('Created %1$d tickets. In %2$s seconds, with %3$d runs to find unique ticket numbers.', 'event-tickets-with-ticket-scanner'), codes.length, secs.slice(0,5), r[1]));
4346 _calcLinesOfCodeTextArea();
4347 },250);
4348 }));
4349
4350 // eingabe maske textarea
4351 function _calcLinesOfCodeTextArea() {
4352 let codesAmount = 0;
4353 input_textarea.val().trim().split('\n').forEach(v=>{
4354 if (v.trim() !== "") codesAmount++;
4355 });
4356 input_textarea_info.html(sprintf(/* translators: %d: amout of ticket numbers */__('contains %d tickets', 'event-tickets-with-ticket-scanner'), codesAmount));
4357 }
4358 let div_textarea = $('<div/>').html('<h3>'+_x('2. Ticket numbers to store on the server', 'title', 'event-tickets-with-ticket-scanner')+'</h3><p>'+__('One number per line and/or comma-separated (,). <br>If you want to add the CVV number then separate your ticket number with (;) and append your CVV number.<br>While storing the numbers to the server, it will check if the ticket number is unique and mark the ones, that are not.', 'event-tickets-with-ticket-scanner')+'</p>').appendTo(div);
4359 let div_textarea_info = $('<div/>').appendTo(div_textarea);
4360 let input_textarea = $('<textarea>').change(_calcLinesOfCodeTextArea).css("height","135px").css("width","100%").appendTo(div_textarea);
4361 let input_textarea_info = $('<div/>').appendTo(div_textarea);
4362 div_textarea.append("<br>");
4363 _calcLinesOfCodeTextArea();
4364 // list auswahl
4365 let div_code_list = _createDivInput(_x('Assign to this ticket list', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div_textarea);
4366 let input_code_list = $('<select><option value="0">'+_x('None', 'option value', 'event-tickets-with-ticket-scanner')+'</select></select>').appendTo(div_code_list);
4367 DATA_LISTS.forEach(v=>{
4368 input_code_list.append('<option value="'+v.id+'">'+v.name+'</option>');
4369 });
4370 div_textarea.append("<br>");
4371
4372 // additional prem fields
4373 if (isPremium() && PREMIUM.addAddCodeFields) {
4374 div_textarea.append(PREMIUM.addAddCodeFields());
4375 }
4376
4377 // button store codes
4378 if (!isPremium()) div_textarea.append('<b>'+sprintf(/* translators: 1: max amout of ticket numbers 2: premium info */__('You can store up to %1$d. %2$s unlimited', 'event-tickets-with-ticket-scanner'), myAjax._max.codes_total, getLabelPremiumOnly())+'<br>');
4379 let btn_store_codes = $('<button/>');
4380 btn_store_codes.addClass("button-primary").html(_x('Store ticket numbers', 'label', 'event-tickets-with-ticket-scanner')).on("click", function(){
4381 // extract codes and
4382 let codes = [];
4383 let codesLines = input_textarea.val().split("\n").map(x=>x.trim());
4384 codesLines.forEach(x=>{
4385 x.split(",").forEach(y=>{
4386 y = y.trim();
4387 y = destroy_tags(y);
4388 if (y != "") codes.push(y);
4389 });
4390 });
4391 if (codes.length === 0) return;
4392
4393 // sperre btn store codes
4394 btn_store_codes.prop("disabled", true);
4395 input_textarea.prop("disabled", true);
4396
4397 div_textarea_info.append($('<div/>').addClass("notice notice-info").html(__("Each entry will turn green (successful stored) or red (NOT OK - duplicate entry on the server).<br>Scroll down and wait for all to finish.<br>In the textarea below you will find all the successful stored tickets.", 'event-tickets-with-ticket-scanner')));
4398 let _output = $('<ol/>').appendTo(div_textarea_info);
4399 div_textarea_info.append('<h3>'+_x('Successful stored ticket numbers', 'title', 'event-tickets-with-ticket-scanner')+'</h3>');
4400 let output_textarea_codes_done = $('<textarea disabled style="4px solid green;width:100%;height:150px;"></textarea>').appendTo(div_textarea_info);
4401
4402 let list_id = parseInt(input_code_list.val(),10);
4403
4404 function __addCodesInChunks(chunk_size) {
4405 let dlg = $('<div/>').html(_getSpinnerHTML());
4406 dlg.dialog({title:_x('Importing', 'title', 'event-tickets-with-ticket-scanner'),closeOnEscape: true,modal: true, dialogClass: "no-close", close: function(event, ui){ abort=true; } });
4407
4408 let abort = false;
4409 let counter_ok = 0;
4410 let counter_notok = 0;
4411 let counter_all = codes.length;
4412 const array_chunks = (array, chunk_size) => Array(Math.ceil(array.length / chunk_size)).fill().map((_, index) => index * chunk_size).map(begin => array.slice(begin, begin + chunk_size));
4413 let chunks = array_chunks(codes, chunk_size);
4414 function _addCodeChunk(idx) {
4415 if (abort) return;
4416 if (idx >= chunks.length) {
4417 dlg.append('<p>'+__('Import process finished', 'event-tickets-with-ticket-scanner')+'</p>');
4418 $('<center/>').append($('<button class="button-primary" />').html(_x('Ok', 'label', 'event-tickets-with-ticket-scanner')).on("click", ()=>{ closeDialog(dlg); })).appendTo(dlg);
4419 return;
4420 }
4421 let arr = chunks[idx];
4422 arr.forEach(v=>{
4423 let div_info_entry = $('<li data-id="code_'+v+'"/>').html(v);
4424 _output.append(div_info_entry);
4425 });
4426 let attr = {"codes":arr, "list_id":list_id};
4427 if (isPremium() && PREMIUM.addAddCodeFieldsData) {
4428 attr = PREMIUM.addAddCodeFieldsData(div_textarea, attr);
4429 }
4430
4431 _makePost("addCodes", attr, function(data){
4432 counter_ok += data.ok.length;
4433 counter_notok += data.notok.length;
4434 if (myAjax._max.codes_total > 0 && myAjax._max.codes_total <= parseInt(data.total_size)) {
4435 div_textarea_info.prepend('<h3 style="color:red;">'+sprintf(/* translators: %d: total ticket count */_x('Your Limit of %d tickets is reached. Use the premium version to have unlimited tickets', 'title', 'event-tickets-with-ticket-scanner'),myAjax._max.codes_total)+'</h3>');
4436 }
4437 let per = Math.ceil(((counter_ok+counter_notok)/counter_all)*100);
4438 let info_content = '<div style="width:100%;border:1px solid #efefef;background-color:white;"><div style="text-align:center;height:20px;background-color:#428bca;color:white;width:'+per+'%;">'+per+'%</div></div>';
4439 info_content += '<p style="margin-top:20px;">'+_x('Amount', 'title', 'event-tickets-with-ticket-scanner')+': '+(counter_ok+counter_notok)+'/'+counter_all+'<br>'+_x('Ok', 'label', 'event-tickets-with-ticket-scanner')+': '+counter_ok+'<br>'+_x('Not Ok', 'label', 'event-tickets-with-ticket-scanner')+': '+counter_notok+'</p>';
4440 dlg.html(info_content);
4441 data.ok.forEach(_v=> {
4442 _output.find('li[data-id="code_'+_v+'"]').css("color","green").append(' ('+_x('Ok', 'label', 'event-tickets-with-ticket-scanner')+')');
4443 output_textarea_codes_done.append(_v+"\n");
4444 });
4445 data.notok.forEach(_v=> {
4446 _output.find('li[data-id="code_'+_v+'"]').css("color","red").append(' ('+_x('Not Ok', 'label', 'event-tickets-with-ticket-scanner')+')');
4447 });
4448 setTimeout(()=>{
4449 _addCodeChunk(idx+1);
4450 }, 100);
4451 }, function(response){
4452 if (response.data.slice(0,4) === "#208") {
4453 FATAL_ERROR === false && LAYOUT.renderFatalError(response.data);
4454 FATAL_ERROR = true;
4455 }
4456 });
4457 }
4458
4459 if (chunks.length === 0) {
4460 closeDialog(dlg);
4461 } else {
4462 _addCodeChunk(0);
4463 }
4464 } // __addCodesInChunks
4465 __addCodesInChunks(100);
4466
4467 // zeige ok button, der info area leer macht und den btn store codes wieder aktiviert
4468 div_textarea_info.append($('<button/>').addClass("button-primary").css("margin-bottom", "20px").html(_x('Ok', 'label', 'event-tickets-with-ticket-scanner')).on("click", function(){
4469 div_textarea_info.html("");
4470 btn_store_codes.prop("disabled", false);
4471 input_textarea.prop("disabled", false);
4472 window.scrollTo(0,0);
4473 }));
4474
4475 }).appendTo(div_textarea);
4476 DIV.html(div);
4477 });
4478 }
4479 renderAdminPageLayout(cbf) {
4480 function __showMaskExport(totalRecordCount) {
4481 if (!totalRecordCount) totalRecordCount = 0;
4482 let maxRange = totalRecordCount > 40000 ? 40000 : totalRecordCount;
4483 let _options = {
4484 title: _x('Export tickets', 'title', 'event-tickets-with-ticket-scanner'),
4485 modal: true,
4486 minWidth: 400,
4487 minHeight: 200,
4488 buttons: [
4489 {
4490 text: _x('Export', 'label', 'event-tickets-with-ticket-scanner'),
4491 click: function() {
4492 ___submitForm();
4493 }
4494 },
4495 {
4496 text: _x('Cancel', 'label', 'event-tickets-with-ticket-scanner'),
4497 click: function() {
4498 closeDialog(this);
4499 }
4500 }
4501 ]
4502 };
4503 let formdlg = $('<form/>').html('<b>'+_x('Choose your export settings', 'title', 'event-tickets-with-ticket-scanner')+'</b><p>');
4504 formdlg.append(_x('Choose the delimiter for the column values', 'label', 'event-tickets-with-ticket-scanner')+'<br><select name="delimiter"><option value="1">, ('+_x('Comma', 'option value', 'event-tickets-with-ticket-scanner')+')</option><option value="2">; ('+_x('Semicolon', 'option value', 'event-tickets-with-ticket-scanner')+')</option><option value="3">| ('+_x('Pipe', 'option value', 'event-tickets-with-ticket-scanner')+')</option></select><p>');
4505 formdlg.append(_x('Choose a file suffix', 'label', 'event-tickets-with-ticket-scanner')+'<br><select name="suffix"><option value="1">.csv</option><option value="2">.txt</option></select><p>');
4506
4507 let _listChooser = $('<select name="listchooser"><option value="0">'+_x('All', 'option value', 'event-tickets-with-ticket-scanner')+'</option></select>');
4508 for(let a=0;a<DATA_LISTS.length;a++) {
4509 _listChooser.append('<option value="'+DATA_LISTS[a].id+'">'+DATA_LISTS[a].name+'</option>');
4510 }
4511 formdlg.append(_x('Limit export to ticket list', 'label', 'event-tickets-with-ticket-scanner')+'<br>').append(_listChooser).append('<p>');
4512
4513 formdlg.append(_x('Choose a sorting field', 'label', 'event-tickets-with-ticket-scanner')+'<br><select name="orderby"><option value="1" selected>'+_x('Creation date', 'option value', 'event-tickets-with-ticket-scanner')+'</option><option value="2">'+__('Ticket number', 'event-tickets-with-ticket-scanner')+'</option><option value="3">'+__('Ticket display number', 'event-tickets-with-ticket-scanner')+'</option><option value="4">'+_x('List name', 'option value', 'event-tickets-with-ticket-scanner')+'</option></select><p>');
4514 formdlg.append(_x('Choose a sorting direction', 'label', 'event-tickets-with-ticket-scanner')+'<br><select name="orderbydirection"><option value="1" selected>'+_x('Ascending', 'option value', 'event-tickets-with-ticket-scanner')+'</option><option value="2">'+_x('Descending', 'option value', 'event-tickets-with-ticket-scanner')+'</option></select><p>');
4515 formdlg.append(_x('Set a range', 'label', 'event-tickets-with-ticket-scanner')+'<br><i>'+sprintf(/* translators: %d: total record count */__('You have %d tickets stored.', 'event-tickets-with-ticket-scanner'), totalRecordCount)+'<br>'+__('Some systems are slow and the connection timeout interrupts the export, if you have too many tickets. In that case, you can export your tickets in several steps. e.g. 0 and 20000 amount and then 20001 and 20000 amount.', 'event-tickets-with-ticket-scanner')+'</i><br>'+__('Enter your row start (0 = from the first)', 'event-tickets-with-ticket-scanner')+'<br><input type="number" name="rangestart" value="0"><br>'+_x('Enter amount of tickets', 'label', 'event-tickets-with-ticket-scanner')+'<br><input type="number" name="rangeamount" value="'+maxRange+'"><p>');
4516 if (isPremium() && PREMIUM && PREMIUM.addExportTicketsInputFields) {
4517 formdlg.append(PREMIUM.addExportTicketsInputFields());
4518 }
4519 let dlg = $('<div/>').append(formdlg);
4520
4521 dlg.dialog(_options);
4522
4523 let form = dlg.find("form").on("submit", function(event) {
4524 event.preventDefault();
4525 ___submitForm();
4526 });
4527
4528 function ___submitForm() {
4529 let delimiter = dlg.find('select[name="delimiter"]').val();
4530 let filesuffix = dlg.find('select[name="suffix"]').val();
4531 let orderby = dlg.find('select[name="orderby"]').val();
4532 let orderbydirection = dlg.find('select[name="orderbydirection"]').val();
4533 let rangestart = dlg.find('input[name="rangestart"]').val();
4534 let rangeamount = dlg.find('input[name="rangeamount"]').val();
4535 let listchooser = dlg.find('select[name="listchooser"]').val();
4536
4537 let data = {'delimiter':delimiter, 'filesuffix':filesuffix, 'orderby':orderby, 'orderbydirection':orderbydirection, 'rangestart':rangestart, 'rangeamount':rangeamount, 'listchooser':listchooser};
4538 if (isPremium() && PREMIUM && PREMIUM.addExportTicketsInputFieldsData) {
4539 data = PREMIUM.addExportTicketsInputFieldsData(data, dlg);
4540 }
4541
4542 let url = _requestURL('exportTableCodes', data);
4543 closeDialog(dlg);
4544 window.open(url, "_blank");
4545 }
4546 }
4547 function __showMaskList(editValues){
4548 let _options = {
4549 title: editValues !== null ? _x('Edit List', 'title', 'event-tickets-with-ticket-scanner') : _x('Add List', 'title', 'event-tickets-with-ticket-scanner'),
4550 modal: true,
4551 minWidth: 600,
4552 minHeight: 400,
4553 open: function(e) {
4554 //$(e.target).parent().css('background-color','orangered');
4555 },
4556 buttons: [
4557 {
4558 text: _x('Ok', 'label', 'event-tickets-with-ticket-scanner'),
4559 click: function() {
4560 ___submitForm();
4561 }
4562 },
4563 {
4564 text: _x('Cancel', 'label', 'event-tickets-with-ticket-scanner'),
4565 click: function() {
4566 closeDialog(this);
4567 }
4568 }
4569 ]
4570 };
4571 let dlg = $('<div/>').html('<form>'+_x('Name', 'label', 'event-tickets-with-ticket-scanner')+'<br><input name="inputName" type="text" style="width:100%;" required></form>');
4572 dlg.dialog(_options);
4573
4574 dlg.find("form").append($('<p>'+_x('Description', 'label', 'event-tickets-with-ticket-scanner')+'<br><textarea name="desc" style="width:100%;"></textarea></p>'));
4575
4576 if (isPremium()) PREMIUM.addListMaskEditFields(dlg, editValues);
4577 else {
4578 if (_getOptions_isActivatedByKey("oneTimeUseOfRegisterCode")) {
4579 dlg.append($('<p><b>'+sprintf(/* translators: %s: h4 option name */__('Overrule %s per Ticket list', 'event-tickets-with-ticket-scanner'), _getOptions_getLabelByKey("h4"))+'</b> '+getLabelPremiumOnly()+'</p>'));
4580 }
4581 }
4582
4583 let metaObj = [];
4584 if (editValues && typeof editValues.meta !== "undefined" && editValues.meta != "") {
4585 try {
4586 metaObj = JSON.parse(editValues.meta);
4587 } catch(e) {}
4588 }
4589
4590 if (_getOptions_isActivatedByKey("userJSRedirectActiv")) {
4591 dlg.find("form").append($('<p>'+_getOptions_getLabelByKey("userJSRedirectURL")+'<br><input type="text" name="redirecturl" style="width:100%;"></p>'));
4592 }
4593
4594 dlg.find("form").append($('<p><input name="serialformatter" type="checkbox"> '+_x('Overrule the ticket format settings', 'label', 'event-tickets-with-ticket-scanner')+'</p>'));
4595 let extra_div = $('<div>').appendTo(dlg).css("margin-top", "10px").css("margin-left", "24px").css("padding", "10px").css("border", "1px solid black")
4596 .html('<p><b>'+_x('Note', 'label', 'event-tickets-with-ticket-scanner')+':</b> '+__('Will be overridden if you set the ticket number format settings on the product!', 'event-tickets-with-ticket-scanner')+'</p>');
4597 let serialCodeFormatter = _form_fields_serial_format(extra_div);
4598 serialCodeFormatter.setNoNumberOptions();
4599 if (typeof metaObj.formatter !== "undefined" && metaObj.formatter.format != "") {
4600 let formatterValues;
4601 try {
4602 let o = metaObj.formatter.format.replace(new RegExp("\\\\", "g"), "").trim();
4603 formatterValues = JSON.parse(o);
4604 serialCodeFormatter.setFormatterValues(formatterValues);
4605 } catch (e) {}
4606 }
4607 serialCodeFormatter.render();
4608
4609 $('<hr>').appendTo(dlg);
4610 $('<h4>').html(_x('Webhook', 'heading', 'event-tickets-with-ticket-scanner')).appendTo(dlg);
4611 if (!_getOptions_isActivatedByKey("webhooksActiv")) {
4612 $('<div style="color:red">').html(_x('The webhook need to be activated first in the options to be executed, even if the URL is set here.', 'label', 'event-tickets-with-ticket-scanner')).appendTo(dlg);
4613 }
4614 $('<div>').html(_x('URL to your service if the WooCommerce ticket is sold', 'label', 'event-tickets-with-ticket-scanner')).appendTo(dlg);
4615 let meta_webhooks_webhookURLaddwcticketsold = $('<input name="meta_webhooks_webhookURLaddwcticketsold" type="text" style="width:100%;">').appendTo(dlg);
4616
4617 let form = dlg.find("form").on("submit", function(event) {
4618 event.preventDefault();
4619 ___submitForm();
4620 });
4621
4622 if (editValues) {
4623 form[0].elements['inputName'].value = editValues.name;
4624 form[0].elements['inputName'].select();
4625 if (typeof metaObj.desc !== "undefined") {
4626 form[0].elements['desc'].value = metaObj.desc.replace(new RegExp("\\\\", "g"), "").trim();
4627 }
4628 if (typeof metaObj.formatter !== "undefined" && metaObj.formatter.active) {
4629 form[0].elements['serialformatter'].checked = true;
4630 }
4631 if (_getOptions_isActivatedByKey("userJSRedirectActiv") && typeof metaObj.redirect !== "undefined" && metaObj.redirect.url) {
4632 form[0].elements['redirecturl'].value = metaObj.redirect.url.trim();
4633 }
4634 if (typeof metaObj.webhooks != "undefined") {
4635 if (typeof metaObj.webhooks.webhookURLaddwcticketsold != "undefined") {
4636 meta_webhooks_webhookURLaddwcticketsold.val(metaObj.webhooks.webhookURLaddwcticketsold);
4637 }
4638 }
4639 }
4640
4641 function ___submitForm() {
4642 let inputName = form[0].elements['inputName'].value.trim();
4643 if (inputName === "") return;
4644
4645 dlg.html(_getSpinnerHTML());
4646 let _data = {"name":inputName};
4647 _data['meta'] = {"desc":"", "formatter":{}, "webhooks":{}};
4648 _data['meta']['desc'] = form[0].elements['desc'].value.trim();
4649 _data['meta']['formatter']['active'] = form[0].elements['serialformatter'].checked ? 1 : 0;
4650 _data['meta']['formatter']['format'] = JSON.stringify(serialCodeFormatter.getFormatterValues());
4651 if (_getOptions_isActivatedByKey("userJSRedirectActiv")) {
4652 _data['meta']['redirect'] = {"url":form[0].elements['redirecturl'].value.trim()};
4653 }
4654 _data['meta']['webhooks']['webhookURLaddwcticketsold'] = meta_webhooks_webhookURLaddwcticketsold.val().trim();
4655 if (isPremium()) PREMIUM.addListMaskEditFieldsData(_data, form[0], editValues);
4656
4657 form[0].reset();
4658 if (editValues) {
4659 _data.id = editValues.id;
4660 _makePost('editList', _data, result=>{
4661 DATA_LISTS = null;
4662 __renderTabelleListen();
4663 tabelle_codes_datatable.ajax.reload();
4664 setTimeout(function(){closeDialog(dlg);},250);
4665 }, function() {
4666 closeDialog(dlg);
4667 });
4668 } else {
4669 _makePost('addList', _data, result=>{
4670 DATA_LISTS = null;
4671 __renderTabelleListen();
4672 closeDialog(dlg);
4673 }, function(response) {
4674 closeDialog(dlg);
4675 if (response.data.slice(0,1) === "#") {
4676 FATAL_ERROR === false && LAYOUT.renderFatalError(response.data);
4677 FATAL_ERROR = true;
4678 }
4679 });
4680 }
4681 }
4682
4683 } // ende showmaskliste
4684
4685 function __showMaskCode(editValues){
4686 let _options = {
4687 title: editValues !== null ? _x('Edit Ticket', 'title', 'event-tickets-with-ticket-scanner') : _x('Add Ticket', 'title', 'event-tickets-with-ticket-scanner'),
4688 modal: true,
4689 minWidth: 400,
4690 minHeight: 200,
4691 buttons: [
4692 {
4693 text: _x('Ok', 'label', 'event-tickets-with-ticket-scanner'),
4694 click: function() {
4695 ___submitForm();
4696 }
4697 },
4698 {
4699 text: _x('Cancel', 'label', 'event-tickets-with-ticket-scanner'),
4700 click: function() {
4701 $( this ).dialog( "close" );
4702 $( this ).html('');
4703 }
4704 }
4705 ]
4706 };
4707 let dlg = $('<div />').html('<form>'+_x('List', 'label', 'event-tickets-with-ticket-scanner')+'<br><select name="inputListId"><option value="0">'+_x('None', 'option value', 'event-tickets-with-ticket-scanner')+'</option></select></form>');
4708 DATA_LISTS.forEach(v=>{
4709 $(dlg).find('select[name="inputListId"]').append('<option '+(editValues && parseInt(editValues.list_id,10) === parseInt(v.id,10) ? 'selected ':'')+'value="'+v.id+'">'+v.name+'</option>');
4710 });
4711
4712 let elem_cvv = $('<input type="text" size="6" minlength="5" maxlength="4" />');
4713 $('<div/>').css({"margin-top":"10px","margin-bottom": "15px","margin-right": "15px"})
4714 .html(_x('CVV - use 4 digits for best results', 'label', 'event-tickets-with-ticket-scanner')+'<br>')
4715 .append(elem_cvv)
4716 .append('<br><i>'+__('If CVV is set, then your user will be asked to enter also the CVV to check the ticket number.', 'event-tickets-with-ticket-scanner')+'</i>')
4717 .appendTo(dlg.find("form"));
4718
4719 let div_status = $('<div/>');
4720 div_status.append(
4721 $('<select name="inputStatus"/>')
4722 .append('<option '+(editValues.aktiv === "1"?'selected':'')+' value="1">'+_x('is activ', 'option value', 'event-tickets-with-ticket-scanner')+'</option>')
4723 .append('<option '+(editValues.aktiv === "0"?'selected':'')+' '+(!isPremium()?'disabled':'')+' value="0">'+_x('is inactiv', 'option value', 'event-tickets-with-ticket-scanner')+' '+(!isPremium()?getLabelPremiumOnly():'')+'</option>')
4724 .append('<option '+(editValues.aktiv === "2"?'selected':'')+' value="2">'+_x('is stolen', 'label', 'event-tickets-with-ticket-scanner')+'</option>')
4725 )
4726 .appendTo(dlg);
4727
4728 dlg.dialog(_options);
4729
4730 if (editValues) {
4731 if (editValues.cvv) elem_cvv.val(editValues.cvv);
4732 }
4733
4734 if (isPremium()) PREMIUM.addCodeMaskEditFields(dlg, editValues);
4735
4736 let form = dlg.find("form").on("submit", function(event) {
4737 event.preventDefault();
4738 ___submitForm();
4739 });
4740 function ___submitForm() {
4741 let inputListId = parseInt($(dlg).find('select[name="inputListId"]').val(),10);
4742 let inputStatusValue = $(dlg).find('select[name="inputStatus"]').val();
4743 dlg.html(_getSpinnerHTML());
4744 let _data = {"list_id":inputListId, "aktiv":inputStatusValue, "cvv":elem_cvv.val().trim()};
4745 if (isPremium()) PREMIUM.addCodeMaskEditFieldsData(_data, form[0], editValues);
4746 form[0].reset();
4747 if (editValues) {
4748 _data.code = editValues.code;
4749 _makeGet('editCode', _data, ()=>{
4750 tabelle_codes_datatable.ajax.reload();
4751 closeDialog(dlg);
4752 }, function() {
4753 closeDialog(dlg);
4754 });
4755 } else {
4756 alert(__("Use the add option", 'event-tickets-with-ticket-scanner'));
4757 }
4758 }
4759 } // ende __showMaskCode
4760
4761 let id_codes = myAjax.divPrefix+'_tabelle_codes';
4762 let tabelle_liste_datatable;
4763 let tabelle_codes_datatable;
4764 let tabelle_codes = $('<table/>').attr("id", id_codes);
4765 let tplace = $('<div/>');
4766
4767 function __renderTabelleListen() {
4768 getDataLists(()=>{
4769 let id_liste = myAjax.divPrefix+'_tabelle_liste';
4770 let tabelle_liste = $('<table/>').attr("id", id_liste);
4771 tabelle_liste.html('<thead><tr><th align="left">'+_x('Name', 'label', 'event-tickets-with-ticket-scanner')+'</th><th align="left">'+_x('Created', 'label', 'event-tickets-with-ticket-scanner')+'</th><th style="width:300px"></th></tr></thead>');
4772 tplace.html(tabelle_liste);
4773
4774 let table = $('#'+id_liste);
4775 $(table).DataTable().clear().destroy();
4776 tabelle_liste_datatable = $(table).DataTable({
4777 language: {
4778 emptyTable: '<b>You need a ticket list to assign it to the products in order to sell tickets.</b>'
4779 },
4780 "responsive": true,
4781 "visible": true,
4782 "searching": true,
4783 "ordering": true,
4784 "processing": true,
4785 "serverSide": false,
4786 "stateSave": true,
4787 "data": DATA_LISTS,
4788 "order": [[ 0, "asc" ]],
4789 "columns":[
4790 {"data":"name", "orderable":true},
4791 {"data":"time", "orderable":true, "width":80,
4792 "render":function (data, type, row) {
4793 return '<span style="display:none;">'+data+'</span>'+DateFormatStringToDateTimeText(data);
4794 }
4795 },
4796 {"data":null,"orderable":false,"defaultContent":'',"className":"buttons dt-right dt-nowrap","width":180,
4797 "render": function ( data, type, row ) {
4798 return '<div class="et-btn-group"><button class="et-btn-action" data-type="showCodes">'+_x('Tickets', 'label', 'event-tickets-with-ticket-scanner')+'</button><button class="et-btn-action" data-type="edit">'+_x('Edit', 'label', 'event-tickets-with-ticket-scanner')+'</button></div><div class="et-btn-group et-btn-group--danger"><button class="et-btn-action et-btn-action--danger" data-type="deleteAllTickets">'+_x('Delete All Tickets', 'label', 'event-tickets-with-ticket-scanner')+'</button><button class="et-btn-action et-btn-action--danger" data-type="delete">'+_x('Delete', 'label', 'event-tickets-with-ticket-scanner')+'</button></div>';
4799 }
4800 }
4801 ]
4802 });
4803 tabelle_liste.css("width", "100%");
4804 table.on('click', 'button[data-type="showCodes"]', e=>{
4805 let data = tabelle_liste_datatable.row( $(e.target).parents('tr') ).data();
4806 tabelle_codes_datatable.search("LIST:"+data.id).draw();
4807 });
4808 table.on('click', 'button[data-type="edit"]', e=>{
4809 let data = tabelle_liste_datatable.row( $(e.target).parents('tr') ).data();
4810 __showMaskList(data);
4811 });
4812 table.on('click', 'button[data-type="delete"]', e=>{
4813 let data = tabelle_liste_datatable.row( $(e.target).parents('tr') ).data();
4814 let content = $('<div>');
4815 content.append('<p>' + __('Are you sure, you want to delete this list?', 'event-tickets-with-ticket-scanner') + '</p>');
4816 content.append('<p><b>' + data.name + '</b></p>');
4817 content.append('<p>' + __('No ticket will be deleted. Just the list.', 'event-tickets-with-ticket-scanner') + '</p>');
4818 content.append('<hr style="margin:15px 0;">');
4819 let checkboxId = 'delete-list-check-products-' + data.id;
4820 let checkboxWrapper = $('<label for="' + checkboxId + '" style="display:flex;align-items:center;gap:8px;cursor:pointer;">');
4821 let checkbox = $('<input type="checkbox" id="' + checkboxId + '" checked>');
4822 checkboxWrapper.append(checkbox);
4823 checkboxWrapper.append(__('Check if list is used by products', 'event-tickets-with-ticket-scanner'));
4824 content.append(checkboxWrapper);
4825
4826 LAYOUT.renderYesNo(_x('Do you want to delete?', 'title', 'event-tickets-with-ticket-scanner'), content, ()=>{
4827 let _data = {
4828 'id': data.id,
4829 'skip_product_check': !checkbox.is(':checked')
4830 };
4831 _makePost('removeList', _data, result=>{
4832 if (result && result.error === 'list_in_use' && result.products) {
4833 let errorContent = $('<div>');
4834 errorContent.append('<p style="color:#b32d2e;font-weight:bold;">' + __('This list is still assigned to products:', 'event-tickets-with-ticket-scanner') + '</p>');
4835 let productList = $('<ul style="margin:10px 0;padding-left:20px;">');
4836 result.products.forEach(function(product) {
4837 let li = $('<li style="margin:5px 0;">');
4838 if (product.edit_url) {
4839 li.append('<a href="' + product.edit_url + '" target="_blank">' + product.name + '</a> (ID: ' + product.id + ')');
4840 } else {
4841 li.append(product.name + ' (ID: ' + product.id + ')');
4842 }
4843 productList.append(li);
4844 });
4845 errorContent.append(productList);
4846 errorContent.append('<p>' + __('Please reassign these products first, or uncheck the product check option.', 'event-tickets-with-ticket-scanner') + '</p>');
4847 LAYOUT.renderInfoBox(__('Cannot delete list', 'event-tickets-with-ticket-scanner'), errorContent);
4848 } else {
4849 __renderTabelleListen();
4850 tabelle_codes_datatable.ajax.reload();
4851 }
4852 });
4853 });
4854 });
4855 table.on('click', 'button[data-type="deleteAllTickets"]', e=>{
4856 let data = tabelle_liste_datatable.row( $(e.target).parents('tr') ).data();
4857 LAYOUT.renderYesNo(
4858 _x('Delete all tickets?', 'title', 'event-tickets-with-ticket-scanner'),
4859 sprintf(__('Are you sure you want to delete ALL tickets from the list "%s"?', 'event-tickets-with-ticket-scanner'), '<b>'+data.name+'</b>') + '<br><br><span style="color:#b32d2e;">' + __('This action cannot be undone!', 'event-tickets-with-ticket-scanner') + '</span>',
4860 ()=>{
4861 let content = $('<div>');
4862 content.append('<p>' + __('To confirm deletion, type DELETE in the field below:', 'event-tickets-with-ticket-scanner') + '</p>');
4863 let confirmInput = $('<input type="text" style="width:100%;" placeholder="DELETE">');
4864 content.append(confirmInput);
4865 LAYOUT.renderYesNo(
4866 _x('Final confirmation', 'title', 'event-tickets-with-ticket-scanner'),
4867 content,
4868 ()=>{
4869 if (confirmInput.val().trim().toUpperCase() !== 'DELETE') {
4870 alert(__('You must type DELETE to confirm.', 'event-tickets-with-ticket-scanner'));
4871 return;
4872 }
4873 let btn = $(e.target);
4874 btn.prop('disabled', true).text(__('Deleting...', 'event-tickets-with-ticket-scanner'));
4875 _makePost('removeAllCodesFromList', {'list_id': data.id}, result=>{
4876 btn.prop('disabled', false).text(_x('Delete All Tickets', 'label', 'event-tickets-with-ticket-scanner'));
4877 tabelle_codes_datatable.ajax.reload();
4878 if (result && result.deleted !== undefined) {
4879 alert(sprintf(__('%d tickets have been deleted.', 'event-tickets-with-ticket-scanner'), result.deleted));
4880 }
4881 });
4882 }
4883 );
4884 }
4885 );
4886 });
4887 }); // end of loading lists
4888 } // __renderTabelleListen
4889 tabelle_codes.css("width", "100%");
4890
4891 STATE = 'admin';
4892 DIV.html(_getSpinnerHTML());
4893 getOptionsFromServer(optionData=>{
4894 DIV.html('');
4895 DIV.append(this.renderMainBody());
4896
4897 let btn_liste_empty = $('<button/>').addClass("button-secondary").html(__('Empty table', 'event-tickets-with-ticket-scanner')).on("click", ()=>{
4898 LAYOUT.renderYesNo(__('Empty table', 'event-tickets-with-ticket-scanner'), sprintf(/* translators: %s: title list of tickets */__('Do you want to empty the "%s" table? All data will be lost. No ticket will be deleted. Just the lists.', 'event-tickets-with-ticket-scanner'), _x('List of tickets', 'title', 'event-tickets-with-ticket-scanner')), ()=>{
4899 LAYOUT.renderYesNo(__('Empty table - last chance', 'event-tickets-with-ticket-scanner'), sprintf(/* translators: %s: title list of tickets */__('Are you sure? You will not be able to restore the data, except you have a backup of your database. All data will be lost. No ticket will be deleted. Just the lists.', 'event-tickets-with-ticket-scanner'), _x('List of tickets', 'title', 'event-tickets-with-ticket-scanner')), ()=>{
4900 _makeGet('emptyTableLists', null, ()=>{
4901 tabelle_codes_datatable.ajax.reload();
4902 __renderTabelleListen();
4903 });
4904 });
4905 });
4906 });
4907 let btn_liste_new = $('<button/>').addClass("button-primary").html(_x('Add', 'label', 'event-tickets-with-ticket-scanner')).on("click", ()=>{
4908 __showMaskList(null);
4909 });
4910 let grp_liste_danger = $('<div class="et-btn-group et-btn-group--danger"/>').append(btn_liste_empty);
4911 let lbl_liste_desc = $('<span class="et-toolbar-desc"/>').html(__("Organize your tickets in lists. You can assign tickets to a list.", 'event-tickets-with-ticket-scanner'));
4912 this.div_liste.html($('<div class="et-toolbar"/>').css('margin-bottom','10px').append(lbl_liste_desc).append(grp_liste_danger).append(isPremium()?'':$('<span class="et-toolbar-hint"/>').html(' '+sprintf(/* translators: 1: max possible lists amount 2: link to premium */__('Max. %1$d list. Unlimited with %2$s', 'event-tickets-with-ticket-scanner'), myAjax._max.lists, getLabelPremiumOnly())+' ')).append(btn_liste_new));
4913 this.div_liste.append(tplace);
4914
4915 __renderTabelleListen();
4916
4917 let additionalColumn_counter_before_created_field = 0;
4918 let additionalColumn = {customerName:'',customerCompany:'',redeemAmount:'',confirmedCount:''};
4919 if (_getOptions_isActivatedByKey('displayAdminAreaColumnConfirmedCount')) {
4920 additionalColumn.confirmedCount = '<th>'+_x('Confirmed Count', 'label', 'event-tickets-with-ticket-scanner')+'</th>';
4921 }
4922 if (_getOptions_isActivatedByKey('displayAdminAreaColumnBillingName')) {
4923 additionalColumn.customerName = '<th>'+_x('Customer', 'label', 'event-tickets-with-ticket-scanner')+'</th>';
4924 additionalColumn_counter_before_created_field++;
4925 }
4926 if (_getOptions_isActivatedByKey('displayAdminAreaColumnBillingCompany')) {
4927 additionalColumn.customerCompany = '<th>'+_x('Company', 'label', 'event-tickets-with-ticket-scanner')+'</th>';
4928 additionalColumn_counter_before_created_field++;
4929 }
4930 if (_getOptions_isActivatedByKey('displayAdminAreaColumnRedeemedInfo')) {
4931 additionalColumn.redeemAmount = '<th>'+_x('Redeem Amount', 'label', 'event-tickets-with-ticket-scanner')+'</th>';
4932 }
4933
4934 tabelle_codes.html('<thead><tr><th style="text-align:left;padding-left:10px;"><input type="checkbox" data-id="checkAll"></th><th>&nbsp;</th><th align="left">'
4935 +_x('Ticket', 'label', 'event-tickets-with-ticket-scanner')+'</th>'+additionalColumn.customerName+additionalColumn.customerCompany+'<th align="left">'
4936 +_x('List', 'label', 'event-tickets-with-ticket-scanner')+'</th><th align="left">'
4937 +_x('Created', 'label', 'event-tickets-with-ticket-scanner')+'</th>'+additionalColumn.confirmedCount+'<th align="left">'
4938 +_x('Redeemed', 'label', 'event-tickets-with-ticket-scanner')+'</th>'+additionalColumn.redeemAmount+'<th>'
4939 +_x('OrderId', 'label', 'event-tickets-with-ticket-scanner')+'</th><th>CVV</th><th>'
4940 +_x('Status', 'label', 'event-tickets-with-ticket-scanner')+'</th><th ></th></tr></thead><tfoot><th colspan="10" style="text-align:left;font-weight:normal;padding-left:0;padding-bottom:0;"></th></tfoot>');
4941 tabelle_codes.find('input[data-id="checkAll"]').on('click', (e)=> {
4942 let isChecked = $(e.currentTarget).prop('checked');
4943 let found = false;
4944 tabelle_codes.find('input[data-type="select-checkbox"]').each((i,v)=>{
4945 $(v).prop('checked', isChecked);
4946 found = true;
4947 });
4948 if (isChecked && found) {
4949 //drop_codes_bulk.prop("disabled", false);
4950 } else {
4951 //drop_codes_bulk.prop("disabled", true);
4952 }
4953 });
4954 let btn_codes_new = $('<button/>').addClass("button-primary").html(_x('Add', 'label', 'event-tickets-with-ticket-scanner')).on("click", ()=>{
4955 if (tabelle_liste_datatable.page.info().recordsTotal === 0) {
4956 alert(__("You need to create a ticket list first before you can add tickets.", 'event-tickets-with-ticket-scanner'));
4957 } else {
4958 if (!isPremium() && tabelle_codes_datatable.page.info().recordsTotal > myAjax._max.codes_total) {
4959 alert(__("You reached maximum amount of tickets. You need to delete tickets before you can add more new tickets or buy the premium version to have unlimited tickets.", 'event-tickets-with-ticket-scanner'));
4960 } else {
4961 LAYOUT.renderAddCodes();
4962 }
4963 }
4964 });
4965 let btn_codes_empty = $('<button/>').addClass("button-secondary").html(__('Empty table', 'event-tickets-with-ticket-scanner')).on("click", ()=>{
4966 LAYOUT.renderYesNo(__('Empty table', 'event-tickets-with-ticket-scanner'), sprintf(/* translators: %s: name of ticket table */__('Do you want to empty the "%s" table? All data will be lost.', 'event-tickets-with-ticket-scanner'), _x("Event Tickets", 'title', 'event-tickets-with-ticket-scanner')), ()=>{
4967 LAYOUT.renderYesNo(__('Empty table - last chance', 'event-tickets-with-ticket-scanner'), sprintf(/* translators: %s: name of ticket table */__('Are you sure? You will not be able to restore the data, except you have a backup of your database. All data will be lost.', 'event-tickets-with-ticket-scanner'), _x("Event Tickets", 'title', 'event-tickets-with-ticket-scanner')), ()=>{
4968 _makeGet('emptyTableCodes', null, ()=>{
4969 tabelle_codes_datatable.ajax.reload();
4970 });
4971 });
4972 });
4973 });
4974 let btn_codes_reload = $('<button/>').addClass("button-secondary").html(__('Refresh table', 'event-tickets-with-ticket-scanner')).on("click", ()=>{
4975 LAYOUT.renderSpinnerShow();
4976 tabelle_codes_datatable.ajax.reload();
4977 window.setTimeout(()=>{LAYOUT.renderSpinnerHide();}, 1500);
4978 });
4979 let btn_codes_export = $('<button/>').addClass("button-secondary").html(_x('Export tickets', 'label', 'event-tickets-with-ticket-scanner')).on("click", ()=>{
4980 //let url = _requestURL('exportTableCodes', null);
4981 //window.open(url, "_blank");
4982 //console.log(tabelle_codes_datatable.page.info());
4983 __showMaskExport(tabelle_codes_datatable.page.info().recordsTotal);
4984 });
4985 let drop_codes_bulk = $('<select data-id="bulk-code-action" />')
4986 .html('<option value="">'+_x('Bulk Action', 'option value', 'event-tickets-with-ticket-scanner')+'</option>');
4987 //.append('<option value="delete">'+_x('Delete', 'label', 'event-tickets-with-ticket-scanner')+'</option>');
4988 for (var key in BulkActions.codes) {
4989 let entry = BulkActions.codes[key];
4990 drop_codes_bulk.append('<option value="'+key+'">'+entry.label+'</option>');
4991 }
4992 drop_codes_bulk.on('change', ()=>{
4993 let val = drop_codes_bulk.val();
4994 if (val !== "") {
4995 let selectedElems = [];
4996 tabelle_codes.find('input[data-type="select-checkbox"]').each((i,v)=>{
4997 if ($(v).prop("checked")) selectedElems.push(v);
4998 });
4999 if (selectedElems.length) {
5000 let fkt = null;
5001 if (typeof BulkActions.codes[val] == "function") {
5002 fkt = BulkActions.codes[val];
5003 } else {
5004 fkt = BulkActions.codes[val].fkt;
5005 }
5006 fkt && fkt(selectedElems, tabelle_codes_datatable);
5007 } else {
5008 LAYOUT.renderInfoBox(_x('Bulk Action', 'title', 'event-tickets-with-ticket-scanner'), __('Please select at least one ticket first.', 'event-tickets-with-ticket-scanner'));
5009 }
5010 }
5011 drop_codes_bulk.val('');
5012 });
5013 let drop_search = $('<select data-id="filter_type" />');
5014 drop_search.append('<option value="">'+_x('Default search filter', 'option value', 'event-tickets-with-ticket-scanner')+'</option>');
5015 drop_search.append('<option value="LIST:">'+_x('Filter for list id', 'option value', 'event-tickets-with-ticket-scanner')+'</option>');
5016 drop_search.append('<option value="ORDERID:">'+_x('Filter for order id', 'option value', 'event-tickets-with-ticket-scanner')+'</option>');
5017 drop_search.append('<option value="CVV:">'+_x('Filter for cvv value', 'option value', 'event-tickets-with-ticket-scanner')+'</option>');
5018 drop_search.append('<option value="STATUS:">'+_x('Filter for status (1:active, 0:inactive, 2:stolen)', 'option value', 'event-tickets-with-ticket-scanner')+'</option>');
5019 drop_search.append('<option value="REDEEMED:">'+_x('Filter for redeemed status (0:not redeemed yet, 1:redeemed)', 'option value', 'event-tickets-with-ticket-scanner')+'</option>');
5020 drop_search.append('<option value="USERID:">'+_x('Filter for registered user id', 'option value', 'event-tickets-with-ticket-scanner')+'</option>');
5021 drop_search.append('<option value="CUSTOMER:">'+_x('Filter for customer name in billing first and last name', 'option value', 'event-tickets-with-ticket-scanner')+'</option>');
5022 drop_search.append('<option value="PRODUCTID:">'+_x('Filter for product id', 'option value', 'event-tickets-with-ticket-scanner')+'</option>');
5023 drop_search.append('<option value="DAYPERTICKET:">'+_x('Filter for chosen date (enter YYYY-MM-DD)', 'option value', 'event-tickets-with-ticket-scanner')+'</option>');
5024 drop_search.on("change", e=>{
5025 let old_search = tabelle_codes_datatable.search().trim();
5026 let search = drop_search.val();
5027 if (old_search && old_search.length > 0) {
5028 search = old_search + " & " + search;
5029 }
5030 tabelle_codes_datatable.search(search);
5031 });
5032 let grp_codes_util = $('<div class="et-btn-group"/>').append(btn_codes_reload).append(btn_codes_export);
5033 let grp_codes_danger = $('<div class="et-btn-group et-btn-group--danger"/>').append(btn_codes_empty);
5034 this.div_codes
5035 .html($('<div class="et-toolbar"/>').css('margin-bottom','10px')
5036 .append(drop_codes_bulk)
5037 .append(drop_search)
5038 .append(grp_codes_util)
5039 .append(grp_codes_danger)
5040 .append(isPremium()?'':$('<span class="et-toolbar-hint"/>').html(' '+sprintf(/* translators: 1: max amount tickets 2: premium link */__('Max. %1$d tickets. Unlimited with %2$s', 'event-tickets-with-ticket-scanner'), myAjax._max.codes_total, getLabelPremiumOnly())+' ')).append(btn_codes_new));
5041 this.div_codes.append(tabelle_codes);
5042
5043 let table_columns = [
5044 {"data":null,"orderable":false,"defaultContent":'', "render":function (data, type, row) {
5045 return '<input type="checkbox" data-type="select-checkbox" data-key="'+data.id+'" data-code="'+data.code+'">';
5046 }},
5047 {"data":null,"className":'details-control',"orderable":false,"defaultContent":''},
5048 {"data":"code_display", "orderable":true, "render":(data,type,row)=>{
5049 return destroy_tags(data);
5050 }},
5051 {"data":"list_name", "orderable":true, "render":(data,type,row)=>{
5052 return destroy_tags(data);
5053 }},
5054 {"data":"time", "className":"dt-center", "orderable":true,
5055 "render":function (data, type, row) {
5056 return '<span style="display:none;">'+data+'</span>'+DateFormatStringToDateTimeText(data);
5057 }
5058 },
5059 {"data":"redeemed", "orderable":true, "className":"dt-center", "render":function(data, type, row) {
5060 if (data == 1) {
5061 return 'yes';
5062 } else {
5063 return '';
5064 }
5065 }},
5066 {"data":"order_id", "className":"dt-right", "orderable":true},
5067 {"data":null, "orderable":false, "className":"dt-center", "render":function(data, type, row){
5068 return data.cvv === "" ? "" : '****';
5069 }},
5070 {"data":null, "orderable":true, "className":"dt-center", "render":function(data, type, row){
5071 let _stat = '';
5072 if (data.meta != "") {
5073 let metaObj = JSON.parse(data.meta);
5074 if (typeof metaObj['used'] !== "undefined") {
5075 if (metaObj.used.reg_request !== "") _stat = '/used';
5076 }
5077 }
5078 if (data.aktiv === "2") return '<span style="color:red;">'+_x('stolen', 'label', 'event-tickets-with-ticket-scanner')+'</span>'+_stat;
5079 return data.aktiv === "1" ? '<span style="color:green;">'+__('active', 'event-tickets-with-ticket-scanner')+'</span>'+_stat : '<span style="color:grey;">'+_x('is inactiv', 'label', 'event-tickets-with-ticket-scanner')+'</span>'+_stat;
5080 }},
5081 {"data":null,"orderable":false,"defaultContent":'',"className":"buttons dt-right dt-nowrap","width":"120px",
5082 "render": function ( data, type, row ) {
5083 return '<div class="et-btn-group"><button class="et-btn-action" data-type="edit">'+_x('Edit', 'label', 'event-tickets-with-ticket-scanner')+'</button></div><div class="et-btn-group et-btn-group--danger"><button class="et-btn-action et-btn-action--danger" data-type="delete">'+_x('Delete', 'label', 'event-tickets-with-ticket-scanner')+'</button></div>';
5084 }
5085 }
5086 ];
5087 let addition_column_offset = 0;
5088 if (_getOptions_isActivatedByKey('displayAdminAreaColumnBillingName')) {
5089 addition_column_offset++;
5090 table_columns.splice(3, 0, {
5091 "data":"_customer_name","orderable":false
5092 });
5093 }
5094 if (_getOptions_isActivatedByKey('displayAdminAreaColumnBillingCompany')) {
5095 addition_column_offset++;
5096 table_columns.splice(3, 0, {
5097 "data":"_customer_company","orderable":false
5098 });
5099 }
5100 if (_getOptions_isActivatedByKey('displayAdminAreaColumnConfirmedCount')) {
5101 addition_column_offset++;
5102 table_columns.splice(4+addition_column_offset, 0, {
5103 "data":null,"orderable":false,"defaultContent":'',"className":"dt-center",
5104 "render":function(data,type,row) {
5105 let ret = 0;
5106 let metaObj = getCodeObjectMeta(data);
5107 if(!metaObj) return ret;
5108 if (typeof metaObj.confirmedCount != "undefined") {
5109 ret = metaObj.confirmedCount;
5110 }
5111 return ret;
5112 }
5113 });
5114 }
5115 if (_getOptions_isActivatedByKey('displayAdminAreaColumnRedeemedInfo')) {
5116 addition_column_offset++;
5117 table_columns.splice(5+addition_column_offset, 0, {
5118 "data":null,"orderable":false,"defaultContent":'',"className":"dt-center",
5119 "render":function(data,type,row) {
5120 let ret = '';
5121 if (row._max_redeem_amount > 0) {
5122 ret = row._redeemed_counter+'/'+row._max_redeem_amount;
5123 } else {
5124 ret = row._redeemed_counter+'/unlimited';
5125 }
5126 return ret;
5127 }
5128 });
5129 }
5130
5131 tabelle_codes_datatable = $(this.div_codes).find('#'+id_codes).DataTable({
5132 "language": {
5133 emptyTable: '<div style="text-align:left;"><b>'+__('You have no tickets yet.', 'event-tickets-with-ticket-scanner')+'</b>'
5134 + '<p>Tickets (number) can be added by two ways.</p>'
5135 + '<ol>'
5136 + '<li>Automatically with each sale of a ticket product.<br>Please configure a woocommerce product to be a ticket product - recommended<br><a href="https://vollstart.com/event-tickets-quick-start-video" target="_blank">Check out the quick start video</a></li>'
5137 + '<li>Or add ticket numbers upfront to a ticket list<br>Click on the add button to import ticket numbers.<br>For this activate the option <b>wcassignmentReuseNotusedCodes</b></li></ol>'
5138 + '</div>'
5139 },
5140 "responsive": true,
5141 "search": {
5142 "search": typeof PARAS.code !== "undefined" ? encodeURIComponent(PARAS.code.trim()) : ''
5143 },
5144 footerCallback: function(row, data, start, end, display) {
5145 let data_anser = tabelle_codes_datatable.ajax.json();
5146 let text = sprintf(/* translators: 1: amount tickets 2: total amount tickets */__('Redeemed tickets: %1$d (filtered) of %2$d (total redeemed tickets)', 'event-tickets-with-ticket-scanner'), data_anser.redeemedRecordsFiltered, data_anser.redeemedRecordsTotal);
5147 var api = this.api();
5148 $(api.column(1).footer()).html(text);
5149 //$(api.tables().footer()).html(text);
5150 },
5151 "processing": true,
5152 "serverSide": true,
5153 "stateSave": false,
5154 "ajax": {
5155 "url": _requestURL('getCodes'),
5156 "type": 'GET'
5157 },
5158 "order": [[ 4 + additionalColumn_counter_before_created_field, "desc" ]],
5159 "columns": table_columns,
5160 "initComplete": function () {
5161 LAYOUT.renderSpinnerHide();
5162 },
5163 "autowidth":true
5164 });
5165 tabelle_codes.on('click', 'button[data-type="edit"]', function (e) {
5166 let data = tabelle_codes_datatable.row( $(this).parents('tr') ).data();
5167 __showMaskCode(data);
5168 });
5169 tabelle_codes.on('click', 'button[data-type="delete"]', function (e) {
5170 let data = tabelle_codes_datatable.row( $(this).parents('tr') ).data();
5171 LAYOUT.renderYesNo(_x('Do you want to delete?', 'title', 'event-tickets-with-ticket-scanner'), __('Are you sure, you want to delete this ticket?', 'event-tickets-with-ticket-scanner')+'<br><br><b>'+data.code+'</b>', ()=>{
5172 let _data = {'id':data.id};
5173 _makePost('removeCode', _data, result=>{
5174 tabelle_codes_datatable.ajax.reload();
5175 });
5176 });
5177 });
5178 $('#'+id_codes+' tbody').on('click', 'td.details-control', function () {
5179 function ___format(d) {
5180 let metaObj = [];
5181 if (d.meta) {
5182 metaObj = JSON.parse(d.meta);
5183 }
5184 let div = $('<div/>');
5185
5186 // hole das aktuelle Metaobj
5187 function __getData(_codeObj) {
5188 div.html(_getSpinnerHTML());
5189 _makeGet('getMetaOfCode',{'code':d.code}, dataMeta=>{
5190 if (_codeObj) { // um eine Aktualisierung in das codeObj aufzunehmen
5191 _codeObj.meta = JSON.stringify(dataMeta);
5192 updateCodeObject(d, _codeObj);
5193 metaObj = getCodeObjectMeta(d);
5194 }
5195
5196 div.html("");
5197 d.meta = JSON.stringify(dataMeta);
5198 d.metaObj = dataMeta;
5199
5200 let btn_grp = $('<div/>').addClass("btn-group").appendTo(div);
5201 $('<button>').html(_x('Display QR with ticket number', 'label', 'event-tickets-with-ticket-scanner')).appendTo(btn_grp).on("click", e=>{
5202 let id = 'qrcode_'+d.code+'_'+time();
5203 let content = _x('This QR image contains', 'label', 'event-tickets-with-ticket-scanner')+':<br><b>'+d.code+'</b><br><br><div id="'+id+'" style="text-align:center;"></div><script>jQuery("#'+id+'").qrcode("'+d.code+'");</script>';
5204 LAYOUT.renderInfoBox(_x('QR with ticket number', 'title', 'event-tickets-with-ticket-scanner'), content);
5205 });
5206 if (d.metaObj.wc_ticket.is_ticket && typeof d.metaObj.wc_ticket._public_ticket_id !== "undefined" && d.metaObj.wc_ticket._public_ticket_id != "") {
5207 $('<button>').html(_x('Display QR with PUBLIC ticket number', 'label', 'event-tickets-with-ticket-scanner')).appendTo(btn_grp).on("click", e=>{
5208 let id = 'qrcode_'+d.code+'_'+time();
5209 let content = _x('This QR image contains', 'label', 'event-tickets-with-ticket-scanner')+':<br><b>'+d.metaObj.wc_ticket._public_ticket_id+'</b><br>'+_x('Can be used with the ticket scanner', 'label', 'event-tickets-with-ticket-scanner')+'<br><br><div id="'+id+'" style="text-align:center;"></div><script>jQuery("#'+id+'").qrcode("'+d.metaObj.wc_ticket._public_ticket_id+'");</script>';
5210 LAYOUT.renderInfoBox(_x('QR with ticket number', 'title', 'event-tickets-with-ticket-scanner'), content);
5211 });
5212 }
5213 if (d.metaObj.wc_ticket.is_ticket && typeof d.metaObj.wc_ticket._qr_content !== "undefined" && d.metaObj.wc_ticket._qr_content != "") {
5214 $('<button>').html(_x('Display QR with your own QR content', 'label', 'event-tickets-with-ticket-scanner')).appendTo(btn_grp).on("click", e=>{
5215 let id = 'qrcode_own_'+d.code+'_'+time();
5216 let content = _x('This QR image contains', 'label', 'event-tickets-with-ticket-scanner')+':<br><b>'+d.metaObj.wc_ticket._qr_content+'</b><br>'+_x('Can be used with the ticket scanner', 'label', 'event-tickets-with-ticket-scanner')+'<br><br><div id="'+id+'" style="text-align:center;"></div><script>jQuery("#'+id+'").qrcode("'+d.metaObj.wc_ticket._qr_content+'");</script>';
5217 LAYOUT.renderInfoBox(_x('QR with ticket number', 'title', 'event-tickets-with-ticket-scanner'), content);
5218 });
5219 }
5220 if (typeof d.metaObj._QR != "undefined" && typeof d.metaObj._QR.directURL != "undefined" && d.metaObj._QR.directURL != "") {
5221 $('<button>').html(_x('Display QR with URL', 'label', 'event-tickets-with-ticket-scanner')).appendTo(btn_grp).on("click", e=>{
5222 let id = 'qrcode_url_'+d.code+'_'+time();
5223 let qr_content = d.metaObj._QR.directURL;
5224 let content = _x('This QR image contains', 'label', 'event-tickets-with-ticket-scanner')+':<br><b>'+qr_content+'</b><br><br><div id="'+id+'" style="text-align:center;"></div><script>jQuery("#'+id+'").qrcode("'+qr_content+'");</script>';
5225 LAYOUT.renderInfoBox(_x('QR with URL and code', 'title', 'event-tickets-with-ticket-scanner'), content);
5226 });
5227 }
5228 div.append('<div/>');
5229
5230 // male die Inhalte
5231 div.append('#'+d.id+'<br><b>'+_x('Created', 'label', 'event-tickets-with-ticket-scanner')+':</b> '+DateFormatStringToDateTimeText(d.time)+' ('+d.time+')<br><b>'+__('Ticket number', 'event-tickets-with-ticket-scanner')+':</b> '+d.code+'<br><b>'+__('Ticket display number', 'event-tickets-with-ticket-scanner')+':</b> '+d.code_display+'<br><b>'+_x('Code Verification Value (CVV)', 'label', 'event-tickets-with-ticket-scanner')+':</b> '+(d.cvv == "" ? '-' : d.cvv)+'<br><b>'+__('is active', 'event-tickets-with-ticket-scanner')+':</b> '+(parseInt(d.aktiv,10) === 1?'True':'False'));
5232 if (d.metaObj && d.metaObj.cvv_attempts && d.metaObj.cvv_attempts.count > 0) {
5233 var $cvvInfo = $('<div style="margin-top:8px;padding:8px;background:#fff8e1;border-left:3px solid #ffc107;"></div>');
5234 $cvvInfo.append('<b>' + __('CVV attempts:', 'event-tickets-with-ticket-scanner') + '</b> ' + d.metaObj.cvv_attempts.count + '<br>');
5235 $cvvInfo.append('<b>' + __('Last attempt:', 'event-tickets-with-ticket-scanner') + '</b> ' + (d.metaObj.cvv_attempts.last_at || '-') + '<br>');
5236 if (d.metaObj.cvv_attempts.locked) {
5237 $cvvInfo.append('<b style="color:#d63638">' + __('LOCKED — too many wrong attempts', 'event-tickets-with-ticket-scanner') + '</b><br>');
5238 }
5239 var $resetBtn = $('<button type="button" class="button">' + __('Reset CVV attempts', 'event-tickets-with-ticket-scanner') + '</button>');
5240 $resetBtn.on('click', function() {
5241 if (!confirm(__('Reset CVV attempt counter and unlock this ticket?', 'event-tickets-with-ticket-scanner'))) return;
5242 _makePost('resetCVVAttempts', {id: d.id}, function() {
5243 __getData(null);
5244 });
5245 });
5246 $cvvInfo.append($resetBtn);
5247 div.append($cvvInfo);
5248 }
5249 div.append(_displayCodeDetails(d, metaObj, tabelle_codes_datatable));
5250
5251 div.append('<h3>'+_x('WooCommerce Order', 'title', 'event-tickets-with-ticket-scanner')+'</h3>');
5252 if (!_getOptions_Versions_isActivatedByKey("is_wc_available")) {
5253 div.append($("<div>").css("color", "red").html(__("WooCommerce not found", 'event-tickets-with-ticket-scanner')));
5254 }
5255 div.append('<b>'+_x('OrderId', 'label', 'event-tickets-with-ticket-scanner')+':</b> ' + (parseInt(d.order_id) === 0 ? '-' : '#'+d.order_id+' <a target="_blank" href="post.php?post='+d.order_id+'&action=edit">'+_x('Show in WooCommerce Orders', 'label', 'event-tickets-with-ticket-scanner')+'</a>'));
5256 if (typeof metaObj['woocommerce'] !== "undefined") {
5257 if (metaObj.woocommerce.order_id !== 0) {
5258 div.append($("<div>").html('<b>'+_x('Order from', 'label', 'event-tickets-with-ticket-scanner')+':</b> ').append($('<span>').text(DateFormatStringToDateTimeText(metaObj.woocommerce.creation_date)+' ('+metaObj.woocommerce.creation_date+')')));
5259 if (dataMeta.woocommerce && dataMeta.woocommerce._order_status) {
5260 div.append($("<div>").html('<b>'+_x('Order Status', 'label', 'event-tickets-with-ticket-scanner')+':</b> ').append($('<span>').text(dataMeta.woocommerce._order_status)));
5261 }
5262 if (dataMeta.woocommerce && dataMeta.woocommerce._billing_email) {
5263 div.append($("<div>").html('<b>'+_x('Billing Email', 'label', 'event-tickets-with-ticket-scanner')+':</b> ').append($('<span>').text(dataMeta.woocommerce._billing_email)));
5264 }
5265 div.append($("<div>").html('<b>'+_x('Product Id', 'label', 'event-tickets-with-ticket-scanner')+':</b> ').append($('<span>').html(metaObj.woocommerce.product_id+' <a target="_blank" href="post.php?post='+encodeURIComponent(metaObj.woocommerce.product_id)+'&action=edit">'+_x('Show Product', 'label', 'event-tickets-with-ticket-scanner')+'</a>')));
5266 if (dataMeta.woocommerce && dataMeta.woocommerce._product_name) {
5267 div.append($("<div>").html('<b>'+_x('Product', 'label', 'event-tickets-with-ticket-scanner')+':</b> ').append($('<span>').text(dataMeta.woocommerce._product_name)));
5268 }
5269 if (dataMeta.woocommerce && dataMeta.woocommerce._variation_attributes) {
5270 div.append($("<div>").html('<b>'+_x('Variation', 'label', 'event-tickets-with-ticket-scanner')+':</b> ').append($('<span>').text(dataMeta.woocommerce._variation_attributes)));
5271 }
5272 }
5273 }
5274 if (typeof metaObj.wc_ticket.subs !== "undefined" && metaObj.wc_ticket.subs.length > 0) {
5275 div.append('<h4>'+__('Related Subscriptions', 'event-tickets-with-ticket-scanner')+'</h4>');
5276 metaObj.wc_ticket.subs.forEach(sub=>{
5277 div.append($("<div>").html('<b>'+_x('Subscription Id', 'label', 'event-tickets-with-ticket-scanner')+':</b> ').append($('<span>').html(sub.order_id+' <a target="_blank" href="post.php?post='+encodeURIComponent(sub.order_id)+'&action=edit">'+_x('Show Subscription', 'label', 'event-tickets-with-ticket-scanner')+'</a> ['+DateTime2Text(sub.date)+']')));
5278 });
5279 }
5280 if (parseInt(d.order_id) > 0) {
5281 div.append($('<div style="margin-top:10px;">').html($('<button>').addClass("button-delete").html(_x('Delete WooCommerce order info for this ticket', 'label', 'event-tickets-with-ticket-scanner')).on("click", ()=>{
5282 LAYOUT.renderYesNo(_x('Remove order', 'title', 'event-tickets-with-ticket-scanner'), sprintf(/* translators: %s: ticket number */__('Do you really want to remove your order information of this ticket "%s"? This will also remove the ticket number from the order! For the PREMIUM PLUGIN: It will only remove it from the position of the order. If you have in one order more than one item with ticket number, then it will only remove the ticket number(s) from this item on the order. For the BASIC PLUGIN, it will remove all tickets from all items on this order. Click OK to proceed the removal.', 'event-tickets-with-ticket-scanner'), d.code_display), ()=>{
5283 _makeGet('removeWoocommerceOrderInfoFromCode', {'code':d.code}, _codeObj=>{
5284 //tabelle_codes_datatable.ajax.reload();
5285 __getData(_codeObj);
5286 });
5287 });
5288 })));
5289 }
5290
5291 div.append('<h4>'+__('WooCommerce ticket sale', 'event-tickets-with-ticket-scanner')+'</h4>');
5292 div.append(_displayWCETicket(d, tabelle_codes_datatable));
5293
5294 div.append('<h3>'+__('WooCommerce Purchase Restriction', 'event-tickets-with-ticket-scanner')+'</h3>');
5295 if (typeof metaObj['wc_rp'] !== "undefined") {
5296 if (metaObj.wc_rp.order_id !== 0) {
5297 div.append($("<div>").html('<b>'+_x('Used for Order ID', 'label', 'event-tickets-with-ticket-scanner')+':</b> ').append($('<span>').html('#'+metaObj.wc_rp.order_id+' <a target="_blank" href="post.php?post='+encodeURIComponent(metaObj.wc_rp.order_id)+'&action=edit">'+_x('Open WooCommerce Order', 'label', 'event-tickets-with-ticket-scanner')+'</a>')));
5298 div.append($("<div>").html('<b>'+_x('Order from', 'label', 'event-tickets-with-ticket-scanner')+':</b> ').append($('<span>').text(metaObj.wc_rp.creation_date)));
5299 div.append($("<div>").html('<b>'+_x('Product Id', 'label', 'event-tickets-with-ticket-scanner')+'s:</b> ').append($('<span>').html(metaObj.wc_rp.product_id+' <a target="_blank" href="post.php?post='+encodeURIComponent(metaObj.wc_rp.product_id)+'&action=edit">'+_x('Show Product', 'label', 'event-tickets-with-ticket-scanner')+'</a>')));
5300 div.append($('<div style="margin-top:10px;">').html($('<button>').addClass("button-delete").html(__('Remove purchase ticket information', 'event-tickets-with-ticket-scanner')).on("click", ()=>{
5301 LAYOUT.renderYesNo(__('Remove purchase ticket information', 'event-tickets-with-ticket-scanner'), sprintf(/* translators: %s: ticket nummer */__('Do you really want to remove the purchase ticket information from the order of this ticket "%s"? This will remove the also the ticket(s) from the order items! This ticket can then be reused for purchases. Click OK to proceed the removal.', 'event-tickets-with-ticket-scanner'), d.code_display), ()=>{
5302 _makeGet('removeWoocommerceRstrPurchaseInfoFromCode', {'code':d.code}, _codeObj=>{
5303 //tabelle_codes_datatable.ajax.reload();
5304 __getData(_codeObj);
5305 });
5306 });
5307 })));
5308 } else {
5309 div.append($("<div>").html('<b>'+_x('Used for Order ID', 'label', 'event-tickets-with-ticket-scanner')+':</b> -'));
5310 }
5311 }
5312
5313 div.append('<h3>'+_x('Registered user', 'title', 'event-tickets-with-ticket-scanner')+'</h3>');
5314 div.append(_displayRegisteredUserForCode(d, metaObj, tabelle_codes_datatable));
5315
5316 div.append('<h3>Redeem operations</h3>');
5317 div.append(_displayRedeemOperationsForCode(d, metaObj));
5318
5319 div.append('<h3>'+_x('IP list checked for this ticket', 'title', 'event-tickets-with-ticket-scanner')+'</h3>');
5320 if (isPremium()) {
5321 div.append(PREMIUM.displayTrackedIPsForCode(d.code));
5322 } else {
5323 div.append(getLabelPremiumOnly());
5324 }
5325
5326 if (isPremium() && PREMIUM.displayCodeDetailsAtEnd) div.append(PREMIUM.displayCodeDetailsAtEnd(d, tabelle_codes_datatable, metaObj));
5327
5328 div.append("<hr>");
5329 });
5330 }
5331 __getData();
5332 return div;
5333 }
5334
5335 var tr = $(this).closest('tr');
5336 var row = tabelle_codes_datatable.row( tr );
5337 if ( row.child.isShown() ) {
5338 // This row is already open - close it
5339 row.child.hide();
5340 tr.removeClass('shown');
5341 } else {
5342 // Open this row
5343 row.child( ___format(row.data()) ).show();
5344 tr.addClass('shown');
5345 }
5346 });
5347 cbf && cbf();
5348 }); // end getOptions
5349 } // render layout
5350
5351 renderInfoBox(title, content, displayPlain) {
5352 let _options = {
5353 title: title,
5354 modal: true,
5355 minWidth: 400,
5356 minHeight: 200,
5357 buttons: [{text:_x('Ok', 'label', 'event-tickets-with-ticket-scanner'),
5358 click: function() {
5359 $(this).dialog("close");
5360 $(this).html("");
5361 }}]
5362 };
5363 let dlg = $('<div/>');
5364 if (displayPlain) {
5365 dlg.text(content);
5366 } else {
5367 dlg.html(content);
5368 }
5369 dlg.dialog(_options);
5370 return dlg;
5371 }
5372 renderSpinnerShow() {
5373 this.div_spinner.css("display", "block");
5374 }
5375 renderSpinnerHide() {
5376 this.div_spinner.css("display", "none");
5377 }
5378 renderFatalError(content) {
5379 return LAYOUT.renderInfoBox(_x('Error', 'title', 'event-tickets-with-ticket-scanner'), content);
5380 }
5381 renderYesNo(title, content, cbfYes, cbfNo) {
5382 let _options = {
5383 title: title,
5384 modal: true,
5385 minWidth: 400,
5386 minHeight: 200,
5387 buttons: [{text:_x('Yes', 'label', 'event-tickets-with-ticket-scanner'), click:function(){
5388 $(this).dialog("close");
5389 $(this).html("");
5390 cbfYes && cbfYes(dlg);
5391 }},{text:_x('No', 'label', 'event-tickets-with-ticket-scanner'), click:function(){
5392 $(this).dialog("close");
5393 $(this).html("");
5394 cbfNo && cbfNo();
5395 }}]
5396 };
5397 let dlg = $('<div/>').html(content);
5398 dlg.dialog(_options);
5399 return dlg;
5400 }
5401 }
5402
5403 function _displayCodeDetails(codeObj, metaObj, tabelle) {
5404 let div = $('<div/>');
5405 function __getData(_codeObj) {
5406 if (_codeObj) { // um eine Aktualisierung in das codeObj aufzunehmen
5407 updateCodeObject(codeObj, _codeObj);
5408 }
5409
5410 div.html("");
5411 if (codeObj.meta !== "") {
5412 let metaObj = getCodeObjectMeta(codeObj);
5413 if (typeof metaObj.confirmedCount !== "undefined") {
5414 div.append($('<div/>').html('<b>Confirmed count:</b> '+metaObj.confirmedCount));
5415 if (metaObj.confirmedCount > 0 && metaObj.validation) {
5416 if (metaObj.validation.first_success != "") {
5417 div.append($('<div/>').html('<b>First successful validation at:</b> '+metaObj.validation.first_success));
5418 div.append($('<div/>').html('<b>First successful validation IP:</b> '+metaObj.validation.first_ip));
5419 }
5420 if (metaObj.validation.last_success != "" && metaObj.validation.last_success != metaObj.validation.first_success) {
5421 div.append($('<div/>').html('<b>Last successful validation at:</b> '+metaObj.validation.last_success));
5422 div.append($('<div/>').html('<b>Last successful validation IP:</b> '+metaObj.validation.last_ip));
5423 }
5424 }
5425 }
5426 let btngrp = $('<div style="margin-top:10px;">');
5427 if (typeof metaObj.used !== "undefined") {
5428 div.append("<h3>Code marked as used</h3>");
5429 if (metaObj.used.reg_request !== "") {
5430 div.append($("<div>").html("<b>Request from:</b> ").append($('<span>').text(DateFormatStringToDateTimeText(metaObj.used.reg_request)+' ('+metaObj.used.reg_request+')')));
5431 div.append($("<div>").html("<b>Request by wordpress user:</b> ").append($('<span>').text(metaObj.used.reg_userid)));
5432 if (metaObj.used._reg_username) div.append($("<div>").html("<b>Request by wordpress user:</b> ").append($('<span>').text(metaObj.used._reg_username)));
5433 div.append($("<div>").html("<b>Request from IP:</b> ").append($('<span>').text(metaObj.used.reg_ip)));
5434
5435 btngrp.append($('<button/>').addClass("button-delete").html('Delete ticket used information').on("click", function(){
5436 LAYOUT.renderYesNo('Remove usage information', 'Do you really want to remove the usage information of this ticket "'+codeObj.code_display+'"? This will also reset the "Confirmed count" to 0.', ()=>{
5437 _makeGet('removeUsedInformationFromCode', {'code':codeObj.code}, _codeObj=>{
5438 //tabelle.ajax.reload();
5439 __getData(_codeObj);
5440 });
5441 });
5442 }));
5443 } else {
5444 div.append("Not used - still available");
5445 }
5446
5447 btngrp.append($('<button/>').addClass("button-edit").html('Edit wordpress user information').on("click", function(){
5448 // display eingabe maske für userid
5449 function __showMask(){
5450 let _options = {
5451 title: 'Edit requested wordpress user',
5452 modal: true,
5453 minWidth: 400,
5454 minHeight: 200,
5455 buttons: [
5456 {
5457 id: 'okBtn',
5458 text: "Ok",
5459 click: function() {
5460 ___submitForm();
5461 }
5462 },
5463 {
5464 text: "Cancel",
5465 click: function() {
5466 $( this ).dialog( "close" );
5467 $( this ).html('');
5468 }
5469 }
5470 ]
5471 };
5472 let dlg = $('<div />');
5473 let form = $('<form />').appendTo(dlg);
5474
5475 let elem_userid = $('<input type="number" min="0" value="'+metaObj.used.reg_userid+'" />');
5476 $('<div/>').css({"margin-top":"10px","margin-bottom": "15px","margin-right": "15px"})
5477 .html('Requested wordpress userid<br>')
5478 .append(elem_userid)
5479 .appendTo(form);
5480
5481 dlg.append('<p>Changes will trigger the webhook, if activated.<br>The IP will be updated too. The requested date will only be changed, if it was not set already.</p>');
5482 dlg.dialog(_options);
5483
5484 form.on("submit", function(event) {
5485 event.preventDefault();
5486 ___submitForm();
5487 });
5488 function ___submitForm() {
5489 let reg_userid = intval(elem_userid.val().trim());
5490 dlg.html(_getSpinnerHTML());
5491 let _data = {"reg_userid":reg_userid};
5492 form[0].reset();
5493 _data.code = codeObj.code;
5494 $('#okBtn').remove();
5495 _makeGet('editUseridForUsedInformationFromCode', _data, _codeObj=>{
5496 //tabelle.ajax.reload();
5497 __getData(_codeObj);
5498 closeDialog(dlg);
5499 }, function() {
5500 closeDialog(dlg);
5501 });
5502 }
5503 } // ende __showMask
5504 __showMask();
5505 })); // end button-edit
5506 }
5507 div.append(btngrp);
5508
5509 if (isPremium()) div.append(PREMIUM.displayCodeDetails(codeObj, tabelle, metaObj));
5510 } // endif codeObj.meta !== ""
5511 }
5512 __getData();
5513 return div;
5514 }
5515
5516 function _displayWCETicket(codeObj, tabelle) {
5517 let div = $('<div/>');
5518 function __getData(_codeObj) {
5519 if (_codeObj) { // um eine Aktualisierung in das codeObj aufzunehmen
5520 updateCodeObject(codeObj, _codeObj);
5521 }
5522
5523 div.html("");
5524 let metaObj = getCodeObjectMeta(codeObj);
5525 if(metaObj) {
5526 if (typeof metaObj.wc_ticket != "undefined" && typeof metaObj.wc_ticket.day_per_ticket != "undefined") {
5527 div.append($('<div>').html('<b>Date per Ticket (chosen by customer):</b> '+metaObj.wc_ticket.day_per_ticket +" ").append(
5528 $("<button>").html("Edit").on("click", ()=>{
5529
5530 let _options = {
5531 title: 'Edit Ticket Date',
5532 modal: true,
5533 minWidth: 400,
5534 minHeight: 200,
5535 buttons: [
5536 {
5537 id: 'okBtn',
5538 text: "Ok",
5539 click: function() {
5540 ___submitForm();
5541 }
5542 },
5543 {
5544 text: "Cancel",
5545 click: function() {
5546 $( this ).dialog( "close" );
5547 $( this ).html('');
5548 }
5549 }
5550 ]
5551 };
5552 let dlg = $('<div />');
5553 let form = $('<form />').appendTo(dlg);
5554
5555 let elem_input = $('<input type="date" value="'+metaObj.wc_ticket.day_per_ticket+'" />');
5556 $('<div/>').css({"margin-top":"10px","margin-bottom": "15px","margin-right": "15px"})
5557 .html('Date per Ticket (yyyy-mm-dd).<br><b>Very important to not break the date format and syntax, if you want to change the date!</b><br>')
5558 .append(elem_input)
5559 .appendTo(form);
5560
5561 dlg.dialog(_options);
5562
5563 form.on("submit", function(event) {
5564 event.preventDefault();
5565 ___submitForm();
5566 });
5567 function ___submitForm() {
5568 let v = elem_input.val().trim();
5569 dlg.html(_getSpinnerHTML());
5570 let _data = {"value":v, "key":'wc_ticket.day_per_ticket'};
5571 form[0].reset();
5572 _data.code = codeObj.code;
5573 $('#okBtn').remove();
5574 _makeGet('editTicketMetaEntry', _data, _codeObj=>{
5575 //tabelle.ajax.reload();
5576 __getData(_codeObj);
5577 closeDialog(dlg);
5578 }, function() {
5579 closeDialog(dlg);
5580 });
5581 }
5582 })
5583 ));
5584 }
5585 if (typeof metaObj.wc_ticket != "undefined" && typeof metaObj.wc_ticket.name_per_ticket != "undefined") {
5586 div.append($('<div>').html('<b>Name per Ticket (product detail setting):</b> '+metaObj.wc_ticket.name_per_ticket +" ").append(
5587 $("<button>").html("Edit").on("click", ()=>{
5588
5589 let _options = {
5590 title: 'Edit Ticket Name',
5591 modal: true,
5592 minWidth: 400,
5593 minHeight: 200,
5594 buttons: [
5595 {
5596 id: 'okBtn',
5597 text: "Ok",
5598 click: function() {
5599 ___submitForm();
5600 }
5601 },
5602 {
5603 text: "Cancel",
5604 click: function() {
5605 $( this ).dialog( "close" );
5606 $( this ).html('');
5607 }
5608 }
5609 ]
5610 };
5611 let dlg = $('<div />');
5612 let form = $('<form />').appendTo(dlg);
5613
5614 let elem_input = $('<input type="text" value="'+metaObj.wc_ticket.name_per_ticket+'" />');
5615 $('<div/>').css({"margin-top":"10px","margin-bottom": "15px","margin-right": "15px"})
5616 .html('Name per Ticket<br>')
5617 .append(elem_input)
5618 .appendTo(form);
5619
5620 dlg.dialog(_options);
5621
5622 form.on("submit", function(event) {
5623 event.preventDefault();
5624 ___submitForm();
5625 });
5626 function ___submitForm() {
5627 let v = elem_input.val().trim();
5628 dlg.html(_getSpinnerHTML());
5629 let _data = {"value":v, "key":'wc_ticket.name_per_ticket'};
5630 form[0].reset();
5631 _data.code = codeObj.code;
5632 $('#okBtn').remove();
5633 _makeGet('editTicketMetaEntry', _data, _codeObj=>{
5634 //tabelle.ajax.reload();
5635 __getData(_codeObj);
5636 closeDialog(dlg);
5637 }, function() {
5638 closeDialog(dlg);
5639 });
5640 }
5641 })
5642 ));
5643 }
5644 if (typeof metaObj.wc_ticket != "undefined" && typeof metaObj.wc_ticket.value_per_ticket != "undefined") {
5645 div.append($('<div>').html('<b>Value per Ticket (product detail setting):</b> '+metaObj.wc_ticket.value_per_ticket +" ").append(
5646 $("<button>").html("Edit").on("click", ()=>{
5647
5648 let _options = {
5649 title: 'Edit Ticket Value',
5650 modal: true,
5651 minWidth: 400,
5652 minHeight: 200,
5653 buttons: [
5654 {
5655 id: 'okBtn',
5656 text: "Ok",
5657 click: function() {
5658 ___submitForm();
5659 }
5660 },
5661 {
5662 text: "Cancel",
5663 click: function() {
5664 $( this ).dialog( "close" );
5665 $( this ).html('');
5666 }
5667 }
5668 ]
5669 };
5670 let dlg = $('<div />');
5671 let form = $('<form />').appendTo(dlg);
5672
5673 let elem_input = $('<input type="text" value="'+metaObj.wc_ticket.value_per_ticket+'" />');
5674 $('<div/>').css({"margin-top":"10px","margin-bottom": "15px","margin-right": "15px"})
5675 .html('Value per Ticket<br>')
5676 .append(elem_input)
5677 .appendTo(form);
5678
5679 dlg.dialog(_options);
5680
5681 form.on("submit", function(event) {
5682 event.preventDefault();
5683 ___submitForm();
5684 });
5685 function ___submitForm() {
5686 let v = elem_input.val().trim();
5687 dlg.html(_getSpinnerHTML());
5688 let _data = {"value":v, "key":'wc_ticket.value_per_ticket'};
5689 form[0].reset();
5690 _data.code = codeObj.code;
5691 $('#okBtn').remove();
5692 _makeGet('editTicketMetaEntry', _data, _codeObj=>{
5693 //tabelle.ajax.reload();
5694 __getData(_codeObj);
5695 closeDialog(dlg);
5696 }, function() {
5697 closeDialog(dlg);
5698 });
5699 }
5700 })
5701 ));
5702 }
5703 // Seat information
5704 if (typeof metaObj.wc_ticket != "undefined" && typeof metaObj.wc_ticket.seat_id != "undefined" && metaObj.wc_ticket.seat_id) {
5705 let seatInfo = metaObj.wc_ticket.seat_label || metaObj.wc_ticket.seat_identifier || ('Seat #' + metaObj.wc_ticket.seat_id);
5706 if (metaObj.wc_ticket.seat_category) {
5707 seatInfo += ' (' + metaObj.wc_ticket.seat_category + ')';
5708 }
5709 div.append($('<div>').html('<b>'+__('Seat', 'event-tickets-with-ticket-scanner')+':</b> ' + seatInfo));
5710 }
5711 if (typeof metaObj['woocommerce'] !== "undefined" && metaObj.woocommerce.order_id !== 0 && typeof metaObj.wc_ticket !== "undefined") {
5712 if (metaObj.wc_ticket.set_by_admin > 0) {
5713 div.append($("<div>").html("<b>Ticket set by admin user:</b> ").append($('<span>').text(metaObj.wc_ticket._set_by_admin_username+' ('+metaObj.wc_ticket.set_by_admin+') '+metaObj.wc_ticket.set_by_admin_date)));
5714 }
5715 if (metaObj.wc_ticket.redeemed_date != '') {
5716 div.append($("<div>").html("<b>Redeemed at:</b> ").append($('<span>').text(DateFormatStringToDateTimeText(metaObj.wc_ticket.redeemed_date)+' ('+metaObj.wc_ticket.redeemed_date+')')));
5717 div.append($("<div>").html("<b>Redeemed by wordpress userid:</b> ").append($('<span>').text(metaObj.wc_ticket.userid)));
5718 if (metaObj.wc_ticket._username) div.append($("<div>").html("<b>Redeemed by wordpress user:</b> ").append($('<span>').text(metaObj.wc_ticket._username)));
5719 div.append($("<div>").html("<b>IP while redeemed:</b> ").append($('<span>').text(metaObj.wc_ticket.ip)));
5720 if (metaObj.wc_ticket.redeemed_by_admin > 0) {
5721 div.append($("<div>").html("<b>Redeemed by admin user:</b> ").append($('<span>').text(metaObj.wc_ticket._redeemed_by_admin_username+' ('+metaObj.wc_ticket.redeemed_by_admin+')')));
5722 }
5723 if (metaObj.wc_ticket.redeemed_via_authtoken_id > 0) {
5724 var tokName = metaObj.wc_ticket._redeemed_via_authtoken_name || '';
5725 div.append($("<div>").html("<b>"+__('Redeemed via authtoken:', 'event-tickets-with-ticket-scanner')+"</b> ").append($('<span>').text(tokName+' (#'+metaObj.wc_ticket.redeemed_via_authtoken_id+')')));
5726 }
5727 }
5728 if (metaObj.wc_ticket.is_ticket == 1) {
5729 let _max_redeem_amount = typeof metaObj.wc_ticket._max_redeem_amount !== "undefined" ? metaObj.wc_ticket._max_redeem_amount : 1;
5730 $("<div>").html("<b>Ticket number: </b>"+codeObj.code_display).appendTo(div);
5731 $("<div>").html("<b>Public Ticket number: </b>"+metaObj.wc_ticket._public_ticket_id).appendTo(div);
5732 if (typeof metaObj.wc_ticket.stats_redeemed !== "undefined") {
5733 $("<div>").html("<b>Redeem usage: </b>"+metaObj.wc_ticket.stats_redeemed.length + ' of ' + (_max_redeem_amount == 0 ? 'unlimited' : _max_redeem_amount)).appendTo(div);
5734 }
5735 $("<div>").html('<b>Ticket Page:</b> <a target="_blank" href="'+metaObj.wc_ticket._url+'">Open Ticket Detail Page</a>').appendTo(div);
5736 $("<div>").html('<b>Ticket Page Testmode:</b> <a target="_blank" href="'+metaObj.wc_ticket._url+'?testDesigner=1">Open Ticket Detail Page with template test code</a>').appendTo(div);
5737 $("<div>").html('<b>Ticket PDF:</b> <a target="_blank" href="'+metaObj.wc_ticket._url+'?pdf">Open Ticket PDF</a>').appendTo(div);
5738 $("<div>").html('<b>Ticket PDF Testmode:</b> <a target="_blank" href="'+metaObj.wc_ticket._url+'?pdf&testDesigner=1">Open Ticket PDF with template test code</a>').appendTo(div);
5739 $("<div>").html('<b>Ticket Scanner:</b> <a target="_blank" href="'+_getTicketScannerURL()+encodeURIComponent(metaObj.wc_ticket._public_ticket_id)+'">Open Ticket Scanner with ticket</a>').appendTo(div);
5740 $("<div>").html('<b>Order Ticket Page:</b> <a target="_blank" href="'+metaObj.wc_ticket._order_page_url+'">Open Order Ticket Page</a>').appendTo(div);
5741 $("<div>").html('<b>Order PDF:</b> <a target="_blank" href="'+metaObj.wc_ticket._order_url+'">Open Order Ticket PDF</a>').appendTo(div);
5742 // Congress page link — if this ticket's product has a congress assigned (above the wallet link)
5743 if (myAjax._congressProducts && metaObj.woocommerce && myAjax._congressProducts[metaObj.woocommerce.product_id]) {
5744 let _congUrl = myAjax._congressProducts[metaObj.woocommerce.product_id].replace('__TICKETID__', encodeURIComponent(metaObj.wc_ticket._public_ticket_id));
5745 $("<div>").html('<b>'+_x('Congress', 'label', 'event-tickets-with-ticket-scanner')+':</b> ').append($('<a target="_blank">').attr('href', _congUrl).text(_x('Open congress page', 'label', 'event-tickets-with-ticket-scanner'))).appendTo(div);
5746 }
5747 if (_getOptions_isActivatedByKey('walletVollstartEnable') && metaObj.wc_ticket._wallet_url) {
5748 $("<div>").html('<b>'+__('Wallet Test', 'event-tickets-with-ticket-scanner')+':</b> <a target="_blank" href="'+metaObj.wc_ticket._wallet_url+'">'+__('Import ticket to Vollstart Wallet for testing', 'event-tickets-with-ticket-scanner')+'</a>').appendTo(div);
5749 }
5750 }
5751
5752 let btngrp = $('<div style="margin-top:10px;">').appendTo(div);
5753 if (metaObj.wc_ticket.is_ticket == 1) {
5754 $('<button>').html("Download PDF").appendTo(btngrp).on("click", ()=>{
5755 _downloadFile('downloadPDFTicket', {'code':codeObj.code}, "eventticket_"+codeObj.code+".pdf");
5756 return false;
5757 });
5758 $('<button>').html("Download Ticket Badge").appendTo(btngrp).on("click", ()=>{
5759 _downloadFile('downloadPDFTicketBadge', {'code':codeObj.code}, "eventticket_badge_"+codeObj.code+".pdf");
5760 return false;
5761 });
5762 $('<button>').html("Display QR with URL to PDF").appendTo(btngrp).on("click", e=>{
5763 let id = 'qrcode_'+codeObj.code+'_'+time();
5764 let content = 'This QR image contains:<br><b>'+codeObj.code+'</b><br><br><div id="'+id+'" style="text-align:center;"></div><script>jQuery("#'+id+'").qrcode("'+metaObj.wc_ticket._url+'?pdf");</script>';
5765 LAYOUT.renderInfoBox('QR with URL to PDF', content);
5766 });
5767 }
5768 if (metaObj.wc_ticket.is_ticket == 0) {
5769 $('<button>').html("Set as ticket sale").on("click", ()=>{
5770 LAYOUT.renderYesNo('Set as a ticket', 'Do you want to set this purchased ticket number as a ticket sale?', ()=>{
5771 _makeGet('setWoocommerceTicketForCode', {'code':codeObj.code}, _codeObj=>{
5772 __getData(_codeObj);
5773 });
5774 });
5775 }).appendTo(btngrp);
5776 }
5777 let btn_redeem = $('<button>').addClass("button-delete").html('Redeem ticket').on("click", ()=>{
5778 let reg_userid = (metaObj.user && metaObj.user.reg_userid) ? metaObj.user.reg_userid : 0;
5779 LAYOUT.renderYesNo('Redeem ticket', 'Do you really want to redeem the ticket number "'+codeObj.code_display+'"? Click OK to redeem the ticket.', ()=>{
5780 let userid = prompt('Optional. You can enter a userid you redeem the ticket for', reg_userid);
5781 _makeGet('redeemWoocommerceTicketForCode', {'code':codeObj.code, 'userid':userid}, _codeObj=>{
5782 __getData(_codeObj);
5783 });
5784 });
5785 }).appendTo(btngrp);
5786 let _max_redeem_amount = typeof metaObj.wc_ticket._max_redeem_amount !== "undefined" ? metaObj.wc_ticket._max_redeem_amount : 1;
5787 if (metaObj.wc_ticket.is_ticket == 0 || _max_redeem_amount == 0 || metaObj.wc_ticket.stats_redeemed.length >= _max_redeem_amount) {
5788 btn_redeem.attr("disabled", true);
5789 }
5790
5791 let btn_unredeem = $('<button>').addClass("button-delete").html('Delete redeem information').on("click", ()=>{
5792 LAYOUT.renderYesNo('Remove ticket information', 'Do you really want to remove the information that the ticket number "'+codeObj.code_display+'" is redeemed? Click OK to un-redeem the ticket and allow your customer to use the ticket again.', ()=>{
5793 _makeGet('removeRedeemWoocommerceTicketForCode', {'code':codeObj.code}, _codeObj=>{
5794 __getData(_codeObj);
5795 });
5796 });
5797 }).appendTo(btngrp);
5798 if (metaObj.wc_ticket.is_ticket == 0 || metaObj.wc_ticket.redeemed_date == "") {
5799 btn_unredeem.attr("disabled", true);
5800 }
5801 if (metaObj.wc_ticket.is_ticket == 1 && metaObj.wc_ticket.redeemed_date == "") {
5802 $('<button>').addClass("button-delete").html("Unset Ticket").on("click", ()=>{
5803 LAYOUT.renderYesNo('Remove ticket', 'Do you really want to remove the ticket info from this ticket number? The WooCommerce sale will be set and you need to remove it manually.', ()=>{
5804 _makeGet('removeWoocommerceTicketForCode', {'code':codeObj.code}, _codeObj=>{
5805 __getData(_codeObj);
5806 });
5807 });
5808 }).appendTo(btngrp);
5809 }
5810 }
5811 }
5812 }
5813 __getData();
5814 return div;
5815 }
5816
5817 function _displayRedeemOperationsForCode(d, metaObj) {
5818 let div = $('<div/>');
5819 if (typeof metaObj.wc_ticket.stats_redeemed !== "undefined") {
5820 if (metaObj.wc_ticket.stats_redeemed.length > 0) {
5821 let t = $('<table>').appendTo(div);
5822 t.html('<tr><th>#</th><th>Date</th><th>IP</th><th>By admin</th><th>User ID</th></tr>').appendTo(t);
5823 metaObj.wc_ticket.stats_redeemed.forEach((v,idx)=>{
5824 let tr = $('<tr>').appendTo(t);
5825 $('<td>').html('#'+(idx+1)).appendTo(tr);
5826 $('<td>').html(DateFormatStringToDateTimeText(v.redeemed_date)+' ('+v.redeemed_date+')').appendTo(tr);
5827 $('<td>').html(v.ip).appendTo(tr);
5828 $('<td>').html(v.redeemed_by_admin == 1 ? 'Yes' : 'No').appendTo(tr);
5829 $('<td>').html(v.userid).appendTo(tr);
5830 });
5831 } else {
5832 div.html("no redeem operations yet");
5833 }
5834 }
5835 return div;
5836 }
5837
5838 function _displayRegisteredUserForCode(codeObj, metaObj, tabelle) {
5839 let div = $('<div/>');
5840 function __getData(_codeObj) {
5841 if (_codeObj) { // um eine Aktualisierung in das codeObj aufzunehmen
5842 updateCodeObject(codeObj, _codeObj);
5843 }
5844 div.html("");
5845 let btngrp = $('<div style="margin-top:10px;">');
5846 if (typeof codeObj.meta !== "undefined" && codeObj.meta !== "") {
5847 let metaObj = getCodeObjectMeta(codeObj);
5848 if (metaObj.user.reg_request !== "") {
5849 div.append($("<div>").html("<b>Register value:</b> ").append($('<span>').text(metaObj.user.value)));
5850 div.append($("<div>").html("<b>Register by wordpress userid:</b> ").append($('<span>').text(metaObj.user.reg_userid)));
5851 if (metaObj.user._reg_username) div.append($("<div>").html("<b>Register by wordpress user:</b> ").append($('<span>').text(metaObj.user._reg_username)));
5852 div.append($("<div>").html("<b>Request from:</b> ").append($('<span>').text(metaObj.user.reg_request)));
5853 div.append($("<div>").html("<b>Request from IP:</b> ").append($('<span>').text(metaObj.user.reg_ip)));
5854 btngrp.append($('<button/>').addClass("button-delete").html('Delete registered user information').on("click", function(){
5855 LAYOUT.renderYesNo('Remove register user value', 'Do you really want to remove the registered user value of this ticket "'+codeObj.code_display+'"?', ()=>{
5856 // sende delete user from code operation zum server
5857 div.html(_getSpinnerHTML());
5858 _makeGet('removeUserRegistrationFromCode', {'code':codeObj.code}, _codeObj=>{
5859 //tabelle.ajax.reload();
5860 __getData(_codeObj);
5861 });
5862 });
5863 }));
5864 } else {
5865 div.append("No registration to this ticket done");
5866 }
5867
5868 btngrp.append($('<button/>').addClass("button-edit").html('Edit registered user information').on("click", function(){
5869 // display eingabe maske für value und userid
5870 function __showMask(){
5871 let _options = {
5872 title: 'Edit registered user',
5873 modal: true,
5874 minWidth: 400,
5875 minHeight: 200,
5876 buttons: [
5877 {
5878 id: 'okBtn',
5879 text: "Ok",
5880 click: function() {
5881 ___submitForm();
5882 }
5883 },
5884 {
5885 text: "Cancel",
5886 click: function() {
5887 $( this ).dialog( "close" );
5888 $( this ).html('');
5889 }
5890 }
5891 ]
5892 };
5893 let dlg = $('<div />');
5894 let form = $('<form />').appendTo(dlg);
5895
5896 let elem_value = $('<input type="text" value="'+metaObj.user.value+'" />');
5897 $('<div/>').css({"margin-top":"10px","margin-bottom": "15px","margin-right": "15px"})
5898 .html('Registered value<br>')
5899 .append(elem_value)
5900 //.append('<br><i>If CVV is set, then your user will be asked to enter also the CVV to check the serial code.</i>')
5901 .appendTo(form);
5902 let elem_userid = $('<input type="number" min="0" value="'+metaObj.user.reg_userid+'" />');
5903 $('<div/>').css({"margin-top":"10px","margin-bottom": "15px","margin-right": "15px"})
5904 .html('Registered wordpress userid<br>')
5905 .append(elem_userid)
5906 .appendTo(form);
5907
5908 dlg.append('<p>Changes will trigger the webhook, if activated.<br>The IP will updated too. The registered date will only be changed, if it was not set already.</p>');
5909 dlg.dialog(_options);
5910
5911 form.on("submit", function(event) {
5912 event.preventDefault();
5913 ___submitForm();
5914 });
5915 function ___submitForm() {
5916 let reg_userid = intval(elem_userid.val().trim());
5917 let reg_value = elem_value.val().trim();
5918 dlg.html(_getSpinnerHTML());
5919 let _data = {"value":reg_value, "reg_userid":reg_userid};
5920 form[0].reset();
5921 _data.code = codeObj.code;
5922 $('#okBtn').remove();
5923 _makeGet('editUseridForUserRegistrationFromCode', _data, _codeObj=>{
5924 //tabelle.ajax.reload();
5925 __getData(_codeObj);
5926 closeDialog(dlg);
5927 }, function() {
5928 closeDialog(dlg);
5929 });
5930 }
5931 } // ende __showMask
5932 __showMask();
5933 })); // end button-edit
5934 div.append(btngrp);
5935 if (isPremium()) div.append(PREMIUM.displayRegisteredUserForCode(codeObj, tabelle, metaObj));
5936 } // endif typeof codeObj.meta !== "undefined" && codeObj.meta !== ""
5937 }
5938 __getData();
5939 return div;
5940 }
5941
5942 function addStyleCode(content) {
5943 let c = document.createElement('style');
5944 c.innerHTML = content;
5945 document.getElementsByTagName("head")[0].appendChild(c);
5946 }
5947 function addStyleTag(url, id, onloadfkt, attrListe, loadLatest) {
5948 var script = document.createElement('link');
5949 script.type = 'text/css';
5950 script.rel = "stylesheet";
5951 let myId = id;
5952 if (!myId) myId = url;
5953 if (document.getElementById(id) && document.getElementById(id).src === url) {
5954 onloadfkt && onloadfkt();
5955 return; // prevent re-adding the same tag
5956 }
5957 script.id = id;
5958 if (attrListe) for(var attr in attrListe) script.setAttribute(attr, attrListe[attr]);
5959 script.href = url;
5960 if (loadLatest) script.href += '?t='+new Date().getTime();
5961 if (typeof onloadfkt !== "undefined") script.onload = onloadfkt;
5962 document.getElementsByTagName("head")[0].appendChild(script);
5963 }
5964 function addScriptCode(content, id) {
5965 if (typeof system.DYNJS_CACHE.scriptCodeElements === "undefined") {
5966 system.DYNJS_CACHE.scriptCodeElements = {};
5967 }
5968 let c;
5969 if (id && typeof system.DYNJS_CACHE.scriptCodeElements[id] !== "undefined") {
5970 c = system.DYNJS_CACHE.scriptCodeElements[id];
5971 document.getElementsByTagName("head")[0].removeChild(c);
5972 } else {
5973 c = document.createElement('script');
5974 }
5975 c.innerHTML = content;
5976 if (id) {
5977 system.DYNJS_CACHE.scriptCodeElements[id] = c;
5978 }
5979 document.getElementsByTagName("head")[0].appendChild(c);
5980 }
5981 function addScriptTag(url, id, onloadfkt, attrListe, loadLatest) {
5982 var head = document.getElementsByTagName("head")[0];
5983 var script = document.createElement('script');
5984 script.type = 'text/javascript';
5985 let myId = id;
5986 if (!myId) myId = url;
5987 if (document.getElementById(id) && document.getElementById(id).src === url) {
5988 onloadfkt && onloadfkt();
5989 return; // prevent re-adding the same tag
5990 }
5991 script.id = id;
5992 if (attrListe) for(var attr in attrListe) script.setAttribute(attr, attrListe[attr]);
5993 script.src = url;
5994 if (loadLatest) script.src += '?t='+new Date().getTime();
5995 if (typeof onloadfkt !== "undefined") script.onload = onloadfkt;
5996 head.appendChild(script);
5997 }
5998
5999 function getPremiumProductURL() {
6000 return 'https://vollstart.com/event-tickets-with-ticket-scanner/?utm_source=etwts_plugin&utm_medium=plugin_link&utm_campaign=etwts_upgrade_to_premium';
6001 }
6002 function getLabelPremiumOnly() {
6003 return '[<a href="'+getPremiumProductURL()+'">PREMIUM ONLY</a>]';
6004 }
6005
6006 function _getSpinnerHTML(text) {
6007 let html = '<div class="et-spinner"><span class="lds-dual-ring"></span>';
6008 if (text) html += '<div class="et-spinner-text">' + text + '</div>';
6009 html += '</div>';
6010 return html;
6011 }
6012
6013 function _loadingJSDatatables(cbf) {
6014 let loaded = {};
6015 addStyleCode('table.dataTable tr.shown td.details-control {background: url('+myAjax._plugin_home_url+'/img/details_close.png) no-repeat center center;}td.details-control {background: url('+myAjax._plugin_home_url+'/img/details_open.png) no-repeat center center;cursor: pointer;}');
6016 addStyleTag(myAjax._plugin_home_url+'/3rd/datatables.min.css', 'jquery_dataTables', ()=>{
6017 loaded['1'] = true;
6018 if (loaded['2']) {
6019 cbf && cbf();
6020 }
6021 }, {'crossorigin':"anonymous"});
6022 addScriptTag(myAjax._plugin_home_url+"/3rd/datatables.min.js", 'jquery_dataTables', ()=>{
6023 loaded['2'] = true;
6024 if (loaded['1']) {
6025 cbf && cbf();
6026 }
6027 }, {'crossorigin':"anonymous", "charset":"utf8"});
6028 }
6029
6030 function isPremium() {
6031 return myAjax._isPremium == "1" || myAjax._isPremium === true;
6032 }
6033
6034 var BulkActions = {
6035 'codes': {
6036 'delete': {
6037 "label": _x('Delete', 'label', 'event-tickets-with-ticket-scanner'),
6038 "fkt": (selectedElems, tabelle_codes_datatable)=>{
6039 LAYOUT.renderYesNo('Delete all selected tickets?', 'Are you sure, you want to delete all selected tickets?<br><br>'+selectedElems.length+' tickets will be deleted.', ()=>{
6040 let _data = {'ids':[]};
6041 selectedElems.forEach(v=>{
6042 _data.ids.push($(v).attr("data-key"));
6043 });
6044 _makePost('removeCodes', _data, result=>{
6045 tabelle_codes_datatable.ajax.reload();
6046 });
6047 });
6048 }
6049 },
6050 'remove_marked_used': {
6051 "label": _x("Remove marked as used", 'option', 'event-tickets-with-ticket-scanner'),
6052 "fkt": (selectedElems, tabelle_codes_datatable)=>{
6053 LAYOUT.renderYesNo('Remove marked used?', 'Are you sure, you want to remove the used marked from all selected tickets?<br><br>'+selectedElems.length+' tickets will be changed.', ()=>{
6054 let _data = {'ids':[], 'codes':[]};
6055 selectedElems.forEach(v=>{
6056 _data.ids.push($(v).attr("data-key"));
6057 _data.codes.push($(v).attr("data-code"));
6058 });
6059 _makePost('removeUsedInformationFromCodeBulk', _data, result=>{
6060 tabelle_codes_datatable.ajax.reload();
6061 });
6062 });
6063 }
6064 },
6065 'remove_ticket_redeemed': {
6066 "label": _x("Delete Redeem Information", 'option', 'event-tickets-with-ticket-scanner'),
6067 "fkt": (selectedElems, tabelle_codes_datatable)=>{
6068 LAYOUT.renderYesNo('Delete the redeem information?', 'Are you sure, you want to remove the the information about the redeem operation of the ticket?<br><br>'+selectedElems.length+' tickets will be changed.', ()=>{
6069 let _data = {'ids':[], 'codes':[]};
6070 selectedElems.forEach(v=>{
6071 _data.ids.push($(v).attr("data-key"));
6072 _data.codes.push($(v).attr("data-code"));
6073 });
6074 _makePost('removeRedeemWoocommerceTicketForCodeBulk', _data, result=>{
6075 tabelle_codes_datatable.ajax.reload();
6076 });
6077 });
6078 }
6079 },
6080 'generate_pdf': {
6081 "label": _x("Generate ticket PDF", 'option', 'event-tickets-with-ticket-scanner'),
6082 "fkt": (selectedElems, tabelle_codes_datatable)=>{
6083 LAYOUT.renderYesNo('Generate the ticket PDF?', 'Are you sure, you want to generate the ticket PDFs for the selected tickets? This can take a while an could timeout the server.<br><br>'+selectedElems.length+' tickets will be added in one PDF.', ()=>{
6084 let _data = {'ids':[], 'codes':[]};
6085 selectedElems.forEach(v=>{
6086 _data.ids.push($(v).attr("data-key"));
6087 _data.codes.push($(v).attr("data-code"));
6088 });
6089 _downloadFile('generateOnePDFForTicketsBulk', _data, "tickets_merged.pdf");
6090 });
6091 }
6092 },
6093 'generate_badge': {
6094 "label": _x("Generate badge ticket", 'option', 'event-tickets-with-ticket-scanner'),
6095 "fkt": (selectedElems, tabelle_codes_datatable)=>{
6096 LAYOUT.renderYesNo('Generate the ticket badge PDF?', 'Are you sure, you want to generate the ticket badge PDFs for the selected tickets? This can take a while an could timeout the server.<br><br>'+selectedElems.length+' badges will be added in one PDF.', ()=>{
6097 let _data = {'ids':[], 'codes':[]};
6098 selectedElems.forEach(v=>{
6099 _data.ids.push($(v).attr("data-key"));
6100 _data.codes.push($(v).attr("data-code"));
6101 });
6102 _downloadFile('generateOnePDFForBadgesBulk', _data, "ticketbadges_merged.pdf");
6103 });
6104 }
6105 },
6106 'move_to_list':{
6107 "label": _x("Move to ticket list", 'option', 'event-tickets-with-ticket-scanner'),
6108 "fkt": (selectedElems, tabelle_codes_datatable)=>{
6109 let content = $('<div>');
6110 let div_code_list = _createDivInput(_x('Assign selected tickets to this ticket list', 'label', 'event-tickets-with-ticket-scanner')).appendTo(content);
6111 let input_code_list = $('<select><option value="0">'+_x('None', 'option value', 'event-tickets-with-ticket-scanner')+'</select></select>').appendTo(div_code_list);
6112 DATA_LISTS.forEach(v=>{
6113 input_code_list.append('<option value="'+v.id+'">'+v.name+'</option>');
6114 });
6115 content.append("<br>");
6116 LAYOUT.renderYesNo('Move ticket(s) to ticket list', content, ()=>{
6117 let _data = {'ids':[], 'codes':[], 'list_id':input_code_list.val()};
6118 selectedElems.forEach(v=>{
6119 _data.ids.push($(v).attr("data-key"));
6120 _data.codes.push($(v).attr("data-code"));
6121 });
6122 _makePost('assignTicketListToTicketsBulk', _data, result=>{
6123 tabelle_codes_datatable.ajax.reload();
6124 });
6125 });
6126 }
6127 }
6128 }
6129 }
6130
6131 function addTabCSS() {
6132 $('<style>')
6133 .prop('type', 'text/css')
6134 .html(`
6135 .tabs {
6136 width: 100%;
6137 display: block;
6138 }
6139 .tab-nav {
6140 list-style: none;
6141 padding: 0;
6142 margin: 0;
6143 display: flex;
6144 border-bottom: 1px solid #ccc;
6145 }
6146 .tab-nav li {
6147 margin: 0;
6148 }
6149 .tab-nav a {
6150 display: block;
6151 padding: 10px 20px;
6152 text-decoration: none;
6153 color: #333;
6154 border: 1px solid #ccc;
6155 border-bottom: none;
6156 background: #f9f9f9;
6157 margin-right: 5px;
6158 border-radius: 5px 5px 0 0;
6159 }
6160 .tab-nav a.active {
6161 background: #fff;
6162 border-bottom: 1px solid #fff;
6163 font-weight: bold;
6164 }
6165 .tab-content {
6166 display: none;
6167 padding: 20px;
6168 border: 1px solid #ccc;
6169 border-radius: 0 5px 5px 5px;
6170 background: #fff;
6171 }
6172 `)
6173 .appendTo('head');
6174 }
6175
6176 function getHelperFunktions() {
6177 return {
6178 _getSpinnerHTML:_getSpinnerHTML,
6179 _makePost:_makePost,
6180 _makeGet:_makeGet,
6181 _getMediaData:_getMediaData,
6182 _downloadFile:_downloadFile,
6183 _requestURL:_requestURL,
6184 _getLAYOUT:function(){ return LAYOUT;},
6185 _getDIV:function(){ return DIV;},
6186 _BulkActions:BulkActions,
6187 _closeDialog:closeDialog,
6188 _OPTIONS:function(){ return OPTIONS;},
6189 _getVarSYSTEM:function(){ return system;},
6190 _updateCodeObject:updateCodeObject,
6191 _getCodeObjectMeta:getCodeObjectMeta,
6192 _DateTime2Text:DateTime2Text,
6193 _DateFormatStringToDateTimeText:DateFormatStringToDateTimeText,
6194 _DateFormatStringToDateText:DateFormatStringToDateText,
6195 _compareVersions:compareVersions,
6196 _getBackButtonDiv:getBackButtonDiv,
6197 _addStyleTag:addStyleTag
6198 };
6199 }
6200
6201 function refreshNoncePeriodically() {
6202 // check if the last check of nonce is older than 4 minutes
6203 // do a ping to get the new nonce
6204 setInterval(()=>{
6205 let last_check = DATA.last_nonce_check;
6206 if (last_check == null || last_check == "") {
6207 last_check = 0;
6208 }
6209 let now = new Date().getTime();
6210 if (now - last_check > 240000) {
6211 _makeGet('ping', [], data=>{
6212 }, ()=>{/* silently ignore ping errors (e.g. timeout when tab is frozen) */});
6213 }
6214 }, 60000);
6215 }
6216
6217 function init() {
6218 addStyleCode('.lds-dual-ring {display:inline-block;width:40px;height:40px;}.lds-dual-ring:after {content:" ";display:block;width:28px;height:28px;margin:4px;border-radius:50%;border:3px solid #9333ea;border-color:#9333ea transparent #9333ea transparent;animation:lds-dual-ring 0.8s linear infinite;}@keyframes lds-dual-ring {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}}');
6219 // CSS is now loaded via wp_enqueue_style in PHP
6220 $('.event-tickets-with-ticket-scanner-admin-page').addClass('et-ready');
6221
6222 addScriptTag(myAjax._plugin_home_url+'/3rd/ace/ace.js');
6223
6224 addTabCSS();
6225
6226 DIV = $('#'+myAjax.divId);
6227 DIV.html(_getSpinnerHTML());
6228 LAYOUT = new Layout();
6229 function _init() {
6230 document.body.style.background = "#ffffff";
6231 _loadingJSDatatables(function() {
6232 if (typeof PARAS.display !== "undefined" && PARAS.display == 'options') {
6233 _displayOptionsArea();
6234 } else if (typeof PARAS.display !== "undefined" && PARAS.display == 'support') {
6235 _displaySupportInfoArea();
6236 } else if (typeof PARAS.display !== "undefined" && PARAS.display == 'authtokens') {
6237 _displayAuthTokensArea();
6238 } else if (typeof PARAS.display !== "undefined" && PARAS.display == 'faq') {
6239 _displayFAQArea();
6240 } else if (typeof PARAS.display !== "undefined" && PARAS.display == 'attendance' && isPremium()) {
6241 _displayAttendanceArea();
6242 } else if(typeof PARAS.display !== "undefined" && PARAS.display == 'congress' && isPremium()) {
6243 _displayCongressesArea();
6244 } else {
6245 LAYOUT.renderAdminPageLayout();
6246 }
6247 });
6248 }
6249
6250 if (isPremium() && myAjax._premJS !== "") {
6251 addScriptTag(myAjax._premJS, null, function() {
6252 PREMIUM = new sasoEventticketsPremium(myAjax, getHelperFunktions());
6253 _init();
6254 });
6255 } else {
6256 _init();
6257 }
6258 $('#wpfooter').css('display', 'none');
6259 refreshNoncePeriodically();
6260 }
6261 if (!doNotInit) init();
6262 return {
6263 init: init,
6264 form_fields_serial_format: _form_fields_serial_format,
6265 makePost: _makePost,
6266 getMediaData: _getMediaData
6267 };
6268
6269 }
6270 if (typeof Ajax_sasoEventtickets !== "undefined") {
6271 window.sasoEventtickets_backend = sasoEventtickets(Ajax_sasoEventtickets);
6272 }
6273
6274 /**
6275 * Global handler for the "Migrate Options" admin notice button.
6276 * Called via onclick from the admin notice rendered by showOptionsMigrationNotice().
6277 */
6278 function sasoEventticketsMigrateOptions(btn) {
6279 btn.disabled = true;
6280 btn.textContent = '...';
6281 jQuery.post(Ajax_sasoEventtickets.url, {
6282 action: Ajax_sasoEventtickets.action,
6283 a_sngmbh: 'migrateOptionsToCustomTable',
6284 nonce: Ajax_sasoEventtickets.nonce
6285 }, function(response) {
6286 if (response && response.data) {
6287 btn.textContent = 'Done! (' + (response.data.migrated || 0) + ' migrated)';
6288 setTimeout(function() { location.reload(); }, 1500);
6289 } else {
6290 btn.textContent = 'Error';
6291 btn.disabled = false;
6292 }
6293 }).fail(function() {
6294 btn.textContent = 'Error';
6295 btn.disabled = false;
6296 });
6297 }