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 / ticket_scanner.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
ticket_scanner.js
2371 lines
1 jQuery(document).ready(()=>{
2 const { __, _x, _n, sprintf } = wp.i18n;
3 let system = {code:0 /* public ticket number */,
4 nonce:'', data:null /* retrieved data */, redeemed_successfully:false,
5 img_pfad:'',
6 last_scanned_ticket:{code:'', timestamp:0, auto_redeem:false, data:null},
7 last_nonce_check:0,
8 status:'ready', /* ready, retrieved, redeemed, awaiting_cvv, locked */
9 currentCVV:'' /* verified CVV for downstream redeem call */
10 };
11 let myAjax;
12 if (typeof IS_PRETTY_PERMALINK_ACTIVATED === "undefined") {
13 IS_PRETTY_PERMALINK_ACTIVATED = false;
14 }
15
16 let rest_route = '/event-tickets-with-ticket-scanner/ticket/scanner/';
17 let pre_route = '../../../../..';
18 if (typeof Ajax_sasoEventtickets != "undefined" && Ajax_sasoEventtickets.wcTicketCompatibilityModeRestURL != '') {
19 pre_route = Ajax_sasoEventtickets.wcTicketCompatibilityModeRestURL.trim();
20 }
21
22 if (typeof Ajax_sasoEventtickets === "undefined") {
23 myAjax = {
24 url: pre_route + '/wp-json'+rest_route
25 };
26 system.nonce = NONCE;
27 } else {
28 myAjax = Ajax_sasoEventtickets;
29 system.nonce = myAjax.nonce;
30 if (Ajax_sasoEventtickets.wcTicketCompatibilityModeRestURL != "") {
31 myAjax.url = Ajax_sasoEventtickets.wcTicketCompatibilityModeRestURL.trim()+'/wp-json'+rest_route;
32 } else {
33 myAjax.url = myAjax._siteUrl+'/wp-json'+rest_route;
34 }
35 IS_PRETTY_PERMALINK_ACTIVATED = myAjax.IS_PRETTY_PERMALINK_ACTIVATED;
36 }
37 myAjax.rest_route = rest_route;
38 myAjax.non_pretty_permalink_url = pre_route+'/?rest_route='+myAjax.rest_route;
39
40 system.INPUTFIELD;
41 system.AUTHTOKENREMOVEBUTTON;
42 system.ADDITIONBUTTONS;
43 system.TIMEAREA;
44
45 function toBool(v) {
46 if (!v) return false;
47 if (v == "1") return true;
48 if (v == 1) return true;
49 if (v.toLowerCase() == "yes") return true;
50 return v == true;
51 }
52
53 var ticket_scanner_operating_option = {
54 redeem_auto: false,
55 distract_free: false,
56 distract_free_show_short_desc: false,
57 speak: false,
58 vibrate: false,
59 auth:"",
60 ticketScannerDontRememberCamChoice:toBool(myAjax.ticketScannerDontRememberCamChoice),
61 ticketScannerStartCamWithoutButtonClicked:false,
62 ticketScannerDontShowOptionControls:toBool(myAjax.ticketScannerDontShowOptionControls),
63 ticketScannerDontShowBtnPDF:toBool(myAjax.ticketScannerDontShowBtnPDF),
64 ticketScannerDontShowBtnBadge:toBool(myAjax.ticketScannerDontShowBtnBadge)
65 };
66
67 var loadingticket = false;
68 var div_ticket_info_area = null;
69 var div_order_info_area = null;
70
71 function addStyleCode(content, media) {
72 let c = document.createElement('style');
73 if (media) c.setAttribute("media", media);
74 c.innerHTML = content;
75 document.getElementsByTagName("head")[0].appendChild(c);
76 }
77
78 function onScanFailure(error) {
79 // handle scan failure, usually better to ignore and keep scanning.
80 // for example:
81 //console.warn(`Code scan error = ${error}`);
82 }
83 var html5QrcodeScanner = null;
84 var qrScanner = null;
85
86 function setStartCamWithoutButtonClicked(value) {
87 if (typeof value != "undefined") {
88 ticket_scanner_operating_option.ticketScannerStartCamWithoutButtonClicked = value;
89 } else {
90 ticket_scanner_operating_option.ticket_scanner_operating_option.ticketScannerStartCamWithoutButtonClicked = !ticket_scanner_operating_option.ticketScannerStartCamWithoutButtonClicked;
91 }
92 _storeValue("ticket_scanner_operating_option.ticketScannerStartCamWithoutButtonClicked", ticket_scanner_operating_option.ticketScannerStartCamWithoutButtonClicked ? 1 : 0);
93 }
94 function setRedeemImmediately(value) {
95 if (typeof value != "undefined") {
96 ticket_scanner_operating_option.redeem_auto = value;
97 } else {
98 ticket_scanner_operating_option.redeem_auto = !ticket_scanner_operating_option.redeem_auto;
99 }
100 _storeValue("ticket_scanner_operating_option.redeem_auto", ticket_scanner_operating_option.redeem_auto ? 1 : 0);
101 }
102 function setDistractFree(value) {
103 if (typeof value != "undefined") {
104 ticket_scanner_operating_option.distract_free = value;
105 } else {
106 ticket_scanner_operating_option.distract_free = !ticket_scanner_operating_option.distract_free;
107 }
108 _storeValue("ticket_scanner_operating_option.distract_free", ticket_scanner_operating_option.distract_free ? 1 : 0);
109 }
110 function setSpeakCheckbox(value) {
111 if (typeof value != "undefined") {
112 ticket_scanner_operating_option.speak = value;
113 } else {
114 ticket_scanner_operating_option.speak = !ticket_scanner_operating_option.speak;
115 }
116 _storeValue("ticket_scanner_operating_option.speak", ticket_scanner_operating_option.speak ? 1 : 0);
117 }
118 function setVibrate(value) {
119 if (typeof value != "undefined") {
120 ticket_scanner_operating_option.vibrate = value;
121 } else {
122 ticket_scanner_operating_option.vibrate = !ticket_scanner_operating_option.vibrate;
123 }
124 _storeValue("ticket_scanner_operating_option.vibrate", ticket_scanner_operating_option.vibrate ? 1 : 0);
125 }
126 function setDistractFreeShowShortDesc(value) {
127 if (typeof value != "undefined") {
128 ticket_scanner_operating_option.distract_free_show_short_desc = value;
129 } else {
130 ticket_scanner_operating_option.distract_free_show_short_desc = !ticket_scanner_operating_option.distract_free_show_short_desc;
131 }
132 _storeValue("ticket_scanner_operating_option.distract_free_show_short_desc", ticket_scanner_operating_option.distract_free_show_short_desc ? 1 : 0);
133 }
134 function initAuthToken() {
135 let text = _loadValue("ticket_scanner_operating_option.auth");
136 if (system.PARA.auth) {
137 text = system.PARA.auth.trim();
138 }
139 if (text != "") {
140 try {
141 let json = JSON.parse(text);
142 setAuthToken(json, true);
143 } catch (e) {
144 alert(e);
145 }
146 }
147 }
148 function setAuthToken(token, doNotUpdateScanOption) {
149 // {"type":"auth","time":"2023-07-10 20:07:24","name":"saso","code":"AHR0CHM6LY92ZXJ3AWNRBHVUZY5KZS93B3JKCHJLC3M=_0C3C7AF3DCCD805F56EF02BEB9E39FFC","areacode":"ticketscanner","url":"https://verwicklung.de/wordpress/wp-content/plugins/event-tickets-with-ticket-scanner/ticket/"}
150 if (typeof token != "undefined" && typeof token.type != "undefined" && token.type == "auth") {
151 //ticket_scanner_operating_option.auth = token;
152 } else {
153 token = "";
154 }
155 ticket_scanner_operating_option.auth = token;
156 _storeValue("ticket_scanner_operating_option.auth", JSON.stringify(token));
157 if (!doNotUpdateScanOption) showScanOptions();
158 }
159 function onScanSuccess(decodedText, decodedResult) {
160 //if (decodedText) decodedText = decodedText.trim();
161 if (system.last_scanned_ticket.code == decodedText && system.last_scanned_ticket.timestamp + 10 > time()) {
162 return;
163 }
164 if (loadingticket) return;
165 loadingticket = true;
166 system.last_scanned_ticket = {code: decodedText, timestamp: time()};
167 updateLastScanTime();
168
169 if (qrScanner != null) {
170 //qrScanner.stop(); // faster if not executed
171 }
172
173 // store setting to cookies / or browser storage
174 if (!ticket_scanner_operating_option.ticketScannerDontRememberCamChoice && html5QrcodeScanner != null) {
175 _storeValue("ticketScannerCameraId", html5QrcodeScanner.persistedDataManager.data.lastUsedCameraId, 365);
176 }
177
178 updateTicketScannerInfoArea("<center>"+sprintf(/* translators: %s: ticket number */__("found %s", 'event-tickets-with-ticket-scanner'), decodedText)+'</center>');
179 // handle the scanned code as you like, for example:
180 //console.log(`Code matched = ${decodedText}`, decodedResult);
181 $("#reader_output").html(__("...loading...", 'event-tickets-with-ticket-scanner'));
182 //window.location.href = "?code="+encodeURIComponent(decodedText) + (ticket_scanner_operating_option.redeem_auto ? "&redeemauto=1" : "");
183
184 let token = null;
185 try {
186 token = JSON.parse(decodedText);
187 } catch(e) {}
188 if (token != null && typeof token == "object") {
189 if (token.type && token.type == "auth") {
190 setAuthToken(token);
191 clearAreas();
192 $("#reader_output").html('');
193 updateTicketScannerInfoArea('<h1 style="color:green !important;">'+__("Auth Token Set", 'event-tickets-with-ticket-scanner')+'</h3>');
194 window.setTimeout(()=>{
195 showScanNextTicketButton();
196 }, 350);
197 } else {
198 renderInfoBox(__("Scan error", 'event-tickets-with-ticket-scanner'), __("QR code content unknown. Can not extract data correctly. Please try a QR code of a ticket.", 'event-tickets-with-ticket-scanner'), showScanNextTicketButton);
199 }
200 } else {
201 /*
202 // not working with QRScanner? or the other scanner. Somehow content is not recognized correctly or not send. maybe a config value to be set. Because with text in it, the scanner is returning an empty string
203 // extract the public ticket number from the token. format is CRC32(TIMESTAMP)-ORDERID-TICKETNUMBER.
204 // the public ticket number can be part of text in the qr code, so we need to extract it.
205 if (decodedText.length > 12) {
206 debugger;
207 // format: NUMBER-NUMBER-TICKETNUMBER , TICKETNUMBER can be text and numbers
208 // example: 2523448324-671-ticket_2025052808_dc_XYJBSSAZGZBHENY
209 reg = /\b\d+-\d+-[A-Za-z0-9_]+\b/g;
210 console.log("decodedText: "+decodedText);
211 let matches = decodedText.match(reg);
212 if (matches && matches.length > 3) {
213 decodedText = matches[0]; // the ticket number is the third match
214 }
215 console.log("extracted ticket number from QR code: "+decodedText);
216 retrieveTicket(decodedText);
217 } else {
218 if (decodedText != "") {
219 renderInfoBox(__("Scan error", 'event-tickets-with-ticket-scanner'), "Cannot find the public ticket number in the QR code. Please try a QR code of a ticket.", showScanNextTicketButton);
220 }
221 }
222 */
223 if (decodedText != "") {
224 retrieveTicket(decodedText);
225 } else {
226 renderInfoBox(__("Scan error", 'event-tickets-with-ticket-scanner'), __("Cannot find the public ticket number in the QR code. Please try a QR code of a ticket.", 'event-tickets-with-ticket-scanner'), showScanNextTicketButton);
227 }
228 }
229
230 if (html5QrcodeScanner != null) {
231 window.setTimeout(()=>{
232 html5QrcodeScanner.clear().then((ignore) => {
233 // QR Code scanning is stopped.
234 // reload the page with the ticket info and redeem button
235 //console.log("stop success");
236 }).catch((err) => {
237 // Stop failed, handle it.
238 //console.log("stop failed");
239 });
240 }, 250);
241 }
242 }
243
244 function startScanner() {
245 if (!ticket_scanner_operating_option.redeem_auto) updateTicketScannerInfoArea("");
246 $("#reader_output").html("");
247 loadingticket = false;
248
249 if (system.PARA.useoldticketscanner) {
250 startScanner_html5QrcodeSCanner();
251 } else {
252 startScanner_QRScanner();
253 }
254 }
255 function startScanner_QRScanner() {
256 let deviceId = _loadValue("ticketScannerCameraId");
257 let v_id = 'saso_eventtickets_qr-video';
258 let camlist_id = 'saso_eventtickets_camList';
259 let start_cam = false;
260 if (document.getElementById(v_id) == null) {
261 $("#reader").html("");
262 start_cam = true;
263 $('#reader').append('<video id="'+v_id+'" style="width:100%" disablepictureinpicture playsinline></video>');
264 $('<select id="'+camlist_id+'" style="width: 100%;"></select>').appendTo($('#reader')).on("change", event=>{
265 _storeValue("ticketScannerCameraId", event.target.value, 365);
266 qrScanner.setCamera(event.target.value);//.then(updateFlashAvailability);
267 });
268 let btn = $('<button>').text("Stop Camera").appendTo($('#reader')).on("click", event=>{
269 qrScanner.stop();
270 qrScanner.destroy();
271 qrScanner = null;
272 btn.css("display", "none");
273 btn_start.css("display", "block");
274 });
275
276 // flashlight button
277 /*
278 https://github.com/nimiq/qr-scanner
279 Flashlight support
280 On supported browsers, you can check whether the currently used camera has a flash and turn it on or off. Note that hasFlash should be called after the scanner was successfully started to avoid the need to open a temporary camera stream just to query whether it has flash support, potentially asking the user for camera access.
281
282 qrScanner.hasFlash(); // check whether the browser and used camera support turning the flash on; async.
283 qrScanner.isFlashOn(); // check whether the flash is on
284 qrScanner.turnFlashOn(); // turn the flash on if supported; async
285 qrScanner.turnFlashOff(); // turn the flash off if supported; async
286 qrScanner.toggleFlash(); // toggle the flash if supported; async.
287 */
288
289 let btn_start = $('<button class="button-ticket-options button-primary" style="display:none;">').text(__("Start Camera", 'event-tickets-with-ticket-scanner')).appendTo($('#reader')).on("click", event=>{
290 btn_start.css("display", "none");
291 btn.css("display", "block");
292 startScanner();
293 });
294 }
295
296 if (qrScanner != null) {
297 qrScanner.stop();
298 qrScanner.destroy();
299 }
300 qrScanner = new QrScanner(
301 document.getElementById(v_id),
302 result => {
303 onScanSuccess(result.data, result);
304 },
305 { highlightScanRegion: true,
306 highlightCodeOutline: true,
307 willReadFrequently:true,
308 /* your options or returnDetailedScanResult: true if you're not specifying any other options */ }
309 );
310
311 if (deviceId != null && deviceId != "" && !ticket_scanner_operating_option.ticketScannerDontRememberCamChoice) {
312 qrScanner.setCamera(deviceId);
313 }
314
315 if (start_cam) {
316 qrScanner.start().then(() => {
317 //updateFlashAvailability();
318 // List cameras after the scanner started to avoid listCamera's stream and the scanner's stream being requested
319 // at the same time which can result in listCamera's unconstrained stream also being offered to the scanner.
320 // Note that we can also start the scanner after listCameras, we just have it this way around in the demo to
321 // start the scanner earlier.
322 QrScanner.listCameras(true).then(cameras => cameras.forEach(camera => {
323 const option = document.createElement('option');
324 option.value = camera.id;
325 option.text = camera.label;
326 if (camera.id == deviceId) {
327 option.selected = true;
328 }
329 $('#'+camlist_id).append(option);
330 }));
331 });
332 } else {
333 qrScanner.start();
334 }
335 }
336 function startScanner_html5QrcodeSCanner() {
337 if (html5QrcodeScanner == null) {
338 let options = { fps: 25, qrbox: {width: 250, height: 250} };
339 let deviceId = _loadValue("ticketScannerCameraId");
340 if (deviceId != null && deviceId != "" && !ticket_scanner_operating_option.ticketScannerDontRememberCamChoice) {
341 options.deviceId = {exact: deviceId}; // deviceId: { exact: cameraId}
342 }
343 html5QrcodeScanner = new Html5QrcodeScanner("reader",
344 options,
345 /* verbose= */ false);
346 }
347 //html5QrcodeScanner.render(onScanSuccess, onScanFailure);
348 html5QrcodeScanner.render(onScanSuccess);
349 window.qrs = html5QrcodeScanner;
350 }
351
352 function showScanNextTicketButton() {
353 let skip = ticket_scanner_operating_option.ticketScannerStartCamWithoutButtonClicked;
354 let div = $('<div>');
355 $('#reader').css("border", "none").html(div);
356 if (skip) {
357 startScanner();
358 } else {
359 let btngrp = $('<div>').css("text-align", 'center').appendTo(div);
360 $('<button class="button-ticket-options button-primary">').html(__("Scan next Ticket", 'event-tickets-with-ticket-scanner')).on("click", e=>{
361 clearAreas();
362 clearOrderInfos();
363 startScanner();
364 }).appendTo(btngrp);
365 if (system.status == "retrieved") {
366 let btn_redeem = $('<button class="button-ticket-options">').html(_x('Redeem Ticket', 'label', 'event-tickets-with-ticket-scanner')).css("background-color", 'gray').css('color', 'white').prop("disabled", true).on('click', e=>{
367 redeemTicket(system.code);
368 }).appendTo(btngrp);
369 if (canTicketBeRedeemed(system.last_scanned_ticket.data)) {
370 btn_redeem.prop("disabled", false).css('background-color','green');
371 }
372 }
373 if (qrScanner != null) {
374 $('<button class="button-ticket-options">').html(__("Stop camera", 'event-tickets-with-ticket-scanner')).on("click", e=>{
375 qrScanner.stop();
376 qrScanner.destroy();
377 qrScanner = null;
378 $(e.target).css("display", "none");
379 }).appendTo(btngrp);
380 }
381 }
382 }
383 function showScanOptions() {
384 let div = $('<div>');
385 if (!ticket_scanner_operating_option.ticketScannerDontShowOptionControls) {
386 let chkbox_speak = $('<input type="checkbox">').on("click", e=> {
387 setSpeakCheckbox();
388 }).appendTo(div);
389 if (ticket_scanner_operating_option.speak) chkbox_speak.prop("checked", true);
390 div.append(' '+__("Speak out loud redeem operation (BETA)", 'event-tickets-with-ticket-scanner'));
391 div.append("<br>");
392
393 let chkbox_vibrate = $('<input type="checkbox">').on("click", e=> {
394 setVibrate();
395 }).appendTo(div);
396 if (ticket_scanner_operating_option.vibrate) chkbox_vibrate.prop("checked", true);
397 div.append(' '+__("Vibrate on scan result", 'event-tickets-with-ticket-scanner'));
398 div.append("<br>");
399
400 let chkbox_redeem_imediately = $('<input type="checkbox">').on("click", e=>{
401 setRedeemImmediately();
402 }).appendTo(div);
403 if (ticket_scanner_operating_option.redeem_auto) chkbox_redeem_imediately.prop("checked", true);
404 div.append(' '+__("Scan and Redeem immediately", 'event-tickets-with-ticket-scanner'));
405 div.append("<br>");
406
407 let chkbox_distractfree = $('<input type="checkbox">').on("click", e=>{
408 setDistractFree();
409 if (ticket_scanner_operating_option.distract_free) {
410 $('#ticket_info').css("display", "none");
411 } else {
412 $('#ticket_info').css("display", "block");
413 }
414 }).appendTo(div);
415 if (ticket_scanner_operating_option.distract_free) chkbox_distractfree.prop("checked", true);
416 div.append(' '+__("Hide ticket information", 'event-tickets-with-ticket-scanner'));
417 div.append("<br>");
418
419 let chkbox_distractfree_showshortdesc = $('<input type="checkbox">').on("click", e=>{
420 setDistractFreeShowShortDesc();
421 if (system.status == "retrieved") {
422 displayTicketRetrievedInfo(system.last_scanned_ticket.data);
423 } else if (system.status == "redeemed") {
424 displayTicketRedeemedInfo(system.data);
425 }
426 }).appendTo(div);
427 if (ticket_scanner_operating_option.distract_free_show_short_desc) chkbox_distractfree_showshortdesc.prop("checked", true);
428 div.append(' '+__("Display short description if ticket information is hidden", 'event-tickets-with-ticket-scanner'));
429 div.append("<br>");
430
431 let chkbox_ticketScannerStartCamWithoutButtonClicked = $('<input type="checkbox">').on("click", e=>{
432 setStartCamWithoutButtonClicked(!ticket_scanner_operating_option.ticketScannerStartCamWithoutButtonClicked);
433 }).appendTo(div);
434 chkbox_ticketScannerStartCamWithoutButtonClicked.prop("checked", ticket_scanner_operating_option.ticketScannerStartCamWithoutButtonClicked);
435 div.append(' '+__("Start cam to scan next ticket immediately", 'event-tickets-with-ticket-scanner'));
436 div.append("<br>");
437
438 $('<input type="checkbox">').on("click", e=>{
439 window.location.href = "?code="+encodeURIComponent(system.code)
440 + (ticket_scanner_operating_option.redeem_auto ? "&redeemauto=1" : "")
441 + (system.PARA.useoldticketscanner ? "" : "&useoldticketscanner=1");
442 }).prop("checked", system.PARA.useoldticketscanner).appendTo(div);
443 div.append(' '+__("Use old ticket scanner library - compatibility mode", 'event-tickets-with-ticket-scanner'));
444 }
445
446 $('<div style="margin-top:40px;">').append(system.INPUTFIELD).appendTo(div);
447 if (typeof ticket_scanner_operating_option.auth == "object") div.append(system.AUTHTOKENREMOVEBUTTON);
448 div.append(system.ADDITIONBUTTONS);
449 addFullscreenButton(div);
450 system.TIMEAREA = $('<div>');
451 div.append(system.TIMEAREA);
452 $('#reader_options').html(div);
453 }
454
455 function addFullscreenButton(container) {
456 var el = document.documentElement;
457 if (!el.requestFullscreen && !el.webkitRequestFullscreen) return;
458 var btn = $('<button class="button-ticket-options">').html(__("Fullscreen", 'event-tickets-with-ticket-scanner'));
459 btn.on("click", function() {
460 if (!document.fullscreenElement && !document.webkitFullscreenElement) {
461 if (el.requestFullscreen) el.requestFullscreen();
462 else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen();
463 } else {
464 if (document.exitFullscreen) document.exitFullscreen();
465 else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
466 }
467 });
468 var onChange = function() {
469 var isFs = !!(document.fullscreenElement || document.webkitFullscreenElement);
470 btn.html(isFs ? __("Exit Fullscreen", 'event-tickets-with-ticket-scanner') : __("Fullscreen", 'event-tickets-with-ticket-scanner'));
471 };
472 document.addEventListener('fullscreenchange', onChange);
473 document.addEventListener('webkitfullscreenchange', onChange);
474 container.append(btn);
475 }
476
477 function vibrateOnResult(success) {
478 if (navigator.vibrate && ticket_scanner_operating_option.vibrate) {
479 navigator.vibrate(success ? [200] : [100, 50, 100, 50, 100]);
480 }
481 }
482
483 function addMetaTag(name, content) {
484 let head = document.getElementsByTagName("head")[0];
485 let metaTags = head.getElementsByTagName("meta");
486 let contains = false;
487 for (let i=0;i<metaTags.length;i++) {
488 let tag = metaTags[i];
489 if (tag.name == name) {
490 tag.content = content;
491 contains = true;
492 break;
493 }
494 }
495 if (!contains) {
496 let metaTag = document.createElement("meta");
497 metaTag.name = name;
498 metaTag.content = content;
499 head.appendChild(metaTag);
500 }
501 }
502
503 function _storeValue(name, wert, days) {
504 if (window.JAVAJSBridge && window.JAVAJSBridge.setItem) window.JAVAJSBridge.setItem(name, wert);
505 else setCookie(name, wert, days);
506 }
507 function _loadValue(name) {
508 if (window.JAVAJSBridge && window.JAVAJSBridge.getItem) return window.JAVAJSBridge.getItem(name);
509 return getCookie(name);
510 }
511 function setCookie(cname, cvalue, exdays) {
512 var d = new Date();
513 if (!exdays) exdays = 30;
514 d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
515 var expires = "expires="+d.toUTCString();
516 document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
517 }
518 function getCookie(cname) {
519 var name = cname + "=";
520 var ca = document.cookie.split(';');
521 for(var i = 0; i < ca.length; i++) {
522 var c = ca[i];
523 while (c.charAt(0) === ' ') {
524 c = c.substring(1);
525 }
526 if (c.indexOf(name) === 0) {
527 return c.substring(name.length, c.length);
528 }
529 }
530 return "";
531 }
532 function _getURLAndDateForAjax(action, myData, pcbf) {
533 let _data = {};
534 _data.action = action;
535 _data.t = new Date().getTime();
536 //if (system.nonce != '') _data._wpnonce = system.nonce;
537 if (system.nonce != '') _data.nonce = system.nonce;
538 pcbf && pcbf();
539 //if (myData) for(var key in myData) _data['data['+key+']'] = myData[key];
540 if (myData) for(var key in myData) _data[key] = myData[key];
541 if (ticket_scanner_operating_option && ticket_scanner_operating_option.auth && ticket_scanner_operating_option.auth.code && ticket_scanner_operating_option.auth.code != "") {
542 let key = "auth";
543 if (Ajax_sasoEventtickets && Ajax_sasoEventtickets._params && Ajax_sasoEventtickets._params.auth) key = Ajax_sasoEventtickets._params.auth;
544 _data[key] = ticket_scanner_operating_option.auth.code;
545 }
546 if (system.nonce != '') {
547 $.ajaxSetup({
548 beforeSend: function(xhr) {
549 xhr.setRequestHeader('X-WP-Nonce', system.nonce);
550 },
551 });
552 }
553
554 // Pass through debug parameter if set in URL
555 var urlParams = new URLSearchParams(window.location.search);
556 if (urlParams.has('VollstartValidatorDebug')) {
557 _data['VollstartValidatorDebug'] = urlParams.get('VollstartValidatorDebug') || '1';
558 }
559
560 let url = myAjax.url;
561 if (IS_PRETTY_PERMALINK_ACTIVATED == false) {
562 url = myAjax.non_pretty_permalink_url;
563 }
564 url += action;
565 return {url:url, data:_data};
566 }
567 function _downloadFile(action, myData, filenameToStore, cbf, ecbf, pcbf) {
568 let call_data = _getURLAndDateForAjax(action, myData, pcbf);
569 let params = "";
570 for(let key in call_data.data) {
571 params += key+"="+encodeURIComponent(call_data.data[key])+"&";
572 }
573 let url = call_data.url+'?'+params;
574 //window.location.href = url;
575 ajax_downloadFile(url, filenameToStore, cbf);
576 }
577 function ajax_downloadFile(urlToSend, fileName, cbf) {
578 var req = new XMLHttpRequest();
579 req.open("GET", urlToSend, true);
580 req.responseType = "blob";
581 req.onload = function (event) {
582 var blob = req.response;
583 //var fileName = req.getResponseHeader("X-fileName") //if you have the fileName header available
584 var link=document.createElement('a');
585 link.href=window.URL.createObjectURL(blob);
586 link.download=fileName;
587 link.click();
588 cbf && cbf();
589 };
590
591 req.send();
592 }
593 function _makeGet(action, myData, cbf, ecbf, pcbf) {
594 let call_data = _getURLAndDateForAjax(action, myData, pcbf);
595 //console.log(call_data);
596 $.get( call_data.url, call_data.data, response=>{
597 if (typeof response == "string") {
598 response = JSON.parse(response);
599 }
600 if (response && response.data && response.data.nonce) {
601 system.last_nonce_check = new Date().getTime();
602 system.nonce = response.data.nonce;
603 }
604 if (!response.success) {
605 if (ecbf) ecbf(response);
606 else {
607 let msg = (typeof response.data !== "undefined" && response.data.status ? response.data.status : '') + " " + (response.data.message ? response.data.message : '');
608 renderFatalError(msg.trim());
609 }
610 } else {
611 cbf && cbf(response.data);
612 }
613 }, "json").always(jqXHR=>{
614 if(jqXHR.status == 401 || jqXHR.status == 403) {
615 renderFatalError(__("Access rights missing. Please login first.", 'event-tickets-with-ticket-scanner') + " "+(jqXHR.responseJSON && jqXHR.responseJSON.message ? jqXHR.responseJSON.message : '') );
616 }
617 if(jqXHR.status == 400) {
618 renderFatalError(jqXHR.responseJSON.message);
619 }
620 });
621 }
622 function _makePost(action, myData, cbf, ecbf, pcbf) {
623 let call_data = _getURLAndDateForAjax(action, myData, pcbf);
624 $.post( call_data.url, call_data.data, response=>{
625 if (typeof response == "string") {
626 response = JSON.parse(response);
627 }
628 if (response && response.data && response.data.nonce) {
629 system.last_nonce_check = new Date().getTime();
630 system.nonce = response.data.nonce;
631 }
632 if (!response.success) {
633 if (ecbf) ecbf(response);
634 else {
635 let msg = (response.data.status ? response.data.status : '') + " " + (response.data.message ? response.data.message : '');
636 renderFatalError(msg.trim());
637 }
638 } else {
639 cbf && cbf(response.data);
640 }
641 }, "json").always(jqXHR=>{
642 if(jqXHR.status == 401 || jqXHR.status == 403) {
643 renderFatalError(__("Access rights missing. Please login first.", 'event-tickets-with-ticket-scanner') + " " + (jqXHR.responseJSON && jqXHR.responseJSON.message ? jqXHR.responseJSON.message : '') );
644 }
645 if(jqXHR.status == 400) {
646 renderFatalError(jqXHR.responseJSON.message);
647 }
648 });
649 }
650 function _getSpinnerHTML() {
651 return '<span class="lds-dual-ring"></span>';
652 }
653 function _getSeatInfoHtml(obj) {
654 if (!obj.seat_label || obj.seat_label == "") {
655 return '';
656 }
657 let seatText = '';
658 if (obj.seat_label_text && obj.seat_label_text != '') {
659 seatText = obj.seat_label_text + ": ";
660 }
661 seatText += "<b>" + obj.seat_label;
662 if (obj.seat_category && obj.seat_category != "") {
663 seatText += " (" + obj.seat_category + ")";
664 }
665 seatText += "</b>";
666 if (obj.seating_plan_name && obj.seating_plan_name != "") {
667 seatText += " - " + obj.seating_plan_name;
668 }
669 if (obj.seat_desc && obj.seat_desc != "") {
670 seatText += "<br><small>" + obj.seat_desc + "</small>";
671 }
672 return seatText;
673 }
674 function makeDateFromString(timestring, timezone_id) {
675 let d = new Date(timestring);
676 return new Date(d.toLocaleString('en', {timeZone: timezone_id}));
677 }
678 function makeDate(timestamp, timezone_id) {
679 let d = new Date();
680 d.setTime(timestamp);
681 return new Date(d.toLocaleString('en', {timeZone: timezone_id}));
682 }
683 function time(timezone_id, timestamp) {
684 let d = new Date();
685 if (timestamp) {
686 d.setTime(timestamp);
687 }
688 if (timezone_id && timezone_id.indexOf("/") > 0) {
689 d = new Date(d.toLocaleString('en', {timeZone: timezone_id}));
690 }
691 return parseInt(d.getTime() / 1000);
692 }
693 function parseDate(str){
694 if (!str) return null;
695 return new Date(str.split(' ')[0].replace(/-/g,"/"));
696 }
697 function parseDateAndText(str, format) {
698 return Date2Text(parseDate(str).getTime(), format);
699 }
700 function DateTime2Text(millisek) {
701 return Date2Text(millisek, system.format_datetime ? system.format_datetime : "d.m.Y H:i");
702 }
703 function Date2Text(millisek, format, timezone_id) {
704 if (!millisek)
705 millisek = time(timezone_id) * 1000;
706 var d = new Date(millisek);
707 if (!format)
708 //format = system.format_date ? system.format_date : "%d.%m.%Y";
709 format = system.format_date ? system.format_date : "d.m.Y";
710 //format = "%d.%m.%Y %H:%i";
711 var tage = [
712 _x('Sun', 'cal', 'event-tickets-with-ticket-scanner'),
713 _x('Mon', 'cal', 'event-tickets-with-ticket-scanner'),
714 _x('Tue', 'cal', 'event-tickets-with-ticket-scanner'),
715 _x('Wed', 'cal', 'event-tickets-with-ticket-scanner'),
716 _x('Thu', 'cal', 'event-tickets-with-ticket-scanner'),
717 _x('Fri', 'cal', 'event-tickets-with-ticket-scanner'),
718 _x('Sat', 'cal', 'event-tickets-with-ticket-scanner')
719 ];
720 var monate = [
721 _x('Jan', 'cal', 'event-tickets-with-ticket-scanner'),
722 _x('Feb', 'cal', 'event-tickets-with-ticket-scanner'),
723 _x('Mar', 'cal', 'event-tickets-with-ticket-scanner'),
724 _x('Apr', 'cal', 'event-tickets-with-ticket-scanner'),
725 _x('May', 'cal', 'event-tickets-with-ticket-scanner'),
726 _x('Jun', 'cal', 'event-tickets-with-ticket-scanner'),
727 _x('Jul', 'cal', 'event-tickets-with-ticket-scanner'),
728 _x('Aug', 'cal', 'event-tickets-with-ticket-scanner'),
729 _x('Sep', 'cal', 'event-tickets-with-ticket-scanner'),
730 _x('Oct', 'cal', 'event-tickets-with-ticket-scanner'),
731 _x('Nov', 'cal', 'event-tickets-with-ticket-scanner'),
732 _x('Dec', 'cal', 'event-tickets-with-ticket-scanner')
733 ];
734 var formate = {'d':d.getDate()<10?'0'+d.getDate():d.getDate(),
735 '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()],
736 'n':d.getMonth()+1,'Y':d.getFullYear(),'y':d.getYear()>100?d.getYear().toString().substring(d.getYear().toString().length-2):d.getYear(),
737 'H':d.getHours()<10?'0'+d.getHours():d.getHours(),'h':d.getHours()>12?d.getHours()-12:d.getHours(),
738 'i':d.getMinutes()<10?'0'+d.getMinutes():d.getMinutes(),'s':d.getSeconds()<10?'0'+d.getSeconds():d.getSeconds()
739 };
740 for (var akey in formate) {
741 //var rg = new RegExp('%'+akey, "g");
742 var rg = new RegExp(akey, "g");
743 format = format.replace(rg, formate[akey]);
744 }
745 return format;
746 }
747 function renderInfoBox(title, content, cbf) {
748 let _options = {
749 title: title,
750 modal: true,
751 minWidth: 400,
752 minHeight: 200,
753 buttons: [{text:_x('Ok', 'label', 'event-tickets-with-ticket-scanner'), click:function(){
754 $(this).dialog("close");
755 $(this).html("");
756 clearAreas();
757 $('#ticket_info').html(content);
758 $('#reader').html("");
759 if (cbf) cbf();
760 }}]
761 };
762 if (typeof content !== "string") content = JSON.stringify(content);
763 let dlg = $('<div/>').html(content);
764 dlg.dialog(_options);
765 return dlg;
766 }
767 function renderFatalError(content, cbf) {
768 return renderInfoBox('Error', content, cbf);
769 }
770 function basics_ermittelURLParameter() {
771 var parawerte = {};
772 var teile;
773 if (window.location.search !== "") {
774 teile = window.location.search.substring(1).split("&");
775 for (var a=0;a<teile.length;a++)
776 {
777 var pos = teile[a].indexOf("=");
778 if (pos < 0) {
779 parawerte[teile[a]] = true;
780 } else {
781 var key = teile[a].substring(0,pos);
782 parawerte[key] = decodeURIComponent(teile[a].substring(pos+1));
783 }
784 }
785 }
786 return parawerte;
787 }
788 function speakOutLoud(v, display) {
789 if ('speechSynthesis' in window) {
790 var t = typeof v === 'object' ? 'Value is an object.' : v;
791 if (t.trim() == "") t = 'Value is empty';
792 var msg = new SpeechSynthesisUtterance(t);
793 msg.lang = "en-US";
794 window.speechSynthesis.speak(msg);
795 if (display) console.log("Speak:", v);
796 } else {
797 console.log(v);
798 }
799 }
800 function clearOrderInfos() {
801 $('#order_info').html("");
802 }
803 function clearAreas() {
804 $('#ticket_info_btns').html('');
805 $('#ticket_add_info').html('');
806 $('#ticket_info').html('');
807 updateTicketScannerInfoArea('');
808 $('#ticket_info_retrieved').html('');
809 }
810 function retrieveTicket(code, redeemed, cbf) {
811 clearAreas();
812 system.currentCVV = ''; // reset stale CVV from prior scan — Task 13 review fix
813 window.scrollBy(0,0);
814 let div = $('#ticket_info').html(_getSpinnerHTML());
815 div.css("display", "block");
816
817 // check if the code is URL
818 if (code.length > 12) {
819 if (code.substring(0,5).toLowerCase() == "https") {
820 if (code.substring(0,8).toLowerCase() == "https://" || (code.length > 14 && code.substring(0,14).toLowerCase() == "https%3A%2F%2F")) {
821 // extract code from URL
822 let url = code;
823 let pos = url.lastIndexOf("code=");
824 if (pos > 0) {
825 code = url.substring(pos + 5);
826 } else {
827 pos = url.toLowerCase().lastIndexOf("code%3d");
828 if (pos > 0) {
829 code = url.substring(pos + 7);
830 }
831 }
832 }
833 }
834 }
835 if (code == "") {
836 alert(__("no code found", 'event-tickets-with-ticket-scanner'));
837 showScanNextTicketButton();
838 return;
839 }
840
841 // check if the code is an order, then transform it to ordertickets
842 if (code.startsWith("order-")) {
843 code = "ordertickets-" + code.substring(6);
844 }
845
846 let redeem = ticket_scanner_operating_option.redeem_auto;
847 if (redeemed == true) {
848 redeem = false; // is already redeemed
849 }
850 system.last_scanned_ticket.auto_redeem = redeem;
851 _makeGet('retrieve_ticket', {'code':code, 'redeem':redeem ? 1 : 0}, data=>{
852 if (ticket_scanner_operating_option.distract_free) {
853 div.css("display", "none");
854 }
855
856 // CVV gate: short-circuit before normal retrieved flow
857 if (data && data.requires_cvv === true) {
858 system.code = code;
859 if (data.locked === true) {
860 system.status = 'locked';
861 showLockedScreen(data);
862 } else {
863 system.status = 'awaiting_cvv';
864 showCVVPrompt(data);
865 }
866 cbf && cbf();
867 return;
868 }
869
870 system.status = "retrieved";
871 system.data = data;
872 system.last_scanned_ticket.data = data;
873 system.code = code; // falls per code überschrieben wurde
874
875 if (typeof data.order_infos !== "undefined" && data.order_infos.is_order_ticket) {
876 system.format_datetime = data.option_displayDateTimeFormat;
877 system.format_date = data.option_displayDateFormat;
878 system.format_time = data.option_displayTimeFormat;
879 displayOrderTicketInfo(data);
880 showScanNextTicketButton();
881 } else {
882 system.format_datetime = data._ret.option_displayDateTimeFormat;
883 system.format_date = data._ret.option_displayDateFormat;
884 system.format_time = data._ret.option_displayTimeFormat;
885 displayTicketInfo(data);
886 displayTicketRetrievedInfo(data);
887 displayTicketAdditionalInfos(data);
888
889 $("#reader_output").html("");
890 if(!redeemed && ticket_scanner_operating_option.redeem_auto && typeof data._ret.redeem_operation !== "undefined") {
891 // display redeem operation
892 //redeemTicket(code);
893 displayRedeemedInfo(code, data._ret.redeem_operation);
894 } else {
895 showScanNextTicketButton();
896 }
897 }
898
899 cbf && cbf();
900 }, response=>{
901 clearAreas();
902 $("#reader_output").html('');
903 updateTicketScannerInfoArea('<h1 style="color:red !important;">'+response.data+'</h3>');
904 showScanNextTicketButton();
905 cbf && cbf();
906 });
907 }
908 // ── CVV prompt / hand-over / hand-back / locked ──────────────────────────
909
910 /**
911 * Stage 1: hand-over screen — customer enters their security code.
912 * @param {Object} data Payload from REST with requires_cvv:true
913 */
914 function showCVVPrompt(data) {
915 var maskByDefault = (typeof Ajax_sasoEventtickets !== 'undefined' && Ajax_sasoEventtickets._cvvMaskInput === true);
916 var oneStage = (typeof Ajax_sasoEventtickets !== 'undefined' && Ajax_sasoEventtickets._cvvOneStage === true);
917 var attemptsLeft = parseInt(data.attempts_remaining, 10);
918
919 clearAreas();
920 var $card = $('<div class="saso-cvv-prompt">');
921 $card.append('<h3>🔒 ' + __('Security code required', 'event-tickets-with-ticket-scanner') + '</h3>');
922 $card.append('<p>' + __('Please hand this device to the customer to enter the security code from their e-mail or profile.', 'event-tickets-with-ticket-scanner') + '</p>');
923
924 var inputType = maskByDefault ? 'password' : 'text';
925 var $inputWrap = $('<div class="saso-cvv-input-wrap">');
926 var $input = $('<input>', {
927 type: inputType,
928 maxlength: 4,
929 'class': 'saso-cvv-input',
930 autocomplete: 'off',
931 autocapitalize: 'characters',
932 inputmode: 'text'
933 });
934 var $toggle = $('<button>', {'type': 'button', 'class': 'button saso-cvv-toggle'})
935 .text(__('Show', 'event-tickets-with-ticket-scanner'));
936 $toggle.on('click', function() {
937 var t = $input.attr('type');
938 $input.attr('type', t === 'password' ? 'text' : 'password');
939 $toggle.text(t === 'password' ? __('Hide', 'event-tickets-with-ticket-scanner') : __('Show', 'event-tickets-with-ticket-scanner'));
940 });
941 $inputWrap.append($input).append($toggle);
942 $card.append($inputWrap);
943
944 var $confirm = $('<button>', {'class': 'button button-primary saso-cvv-confirm'})
945 .text(__('Confirm code', 'event-tickets-with-ticket-scanner'));
946 $card.append($confirm);
947 $card.append('<p class="saso-cvv-attempts">' + __('Attempts remaining:', 'event-tickets-with-ticket-scanner') + ' <b>' + attemptsLeft + '</b></p>');
948
949 updateTicketScannerInfoArea($card);
950 $input.trigger('focus');
951
952 $confirm.on('click', function() {
953 var cvv = $input.val().trim();
954 if (!cvv) return;
955 submitCVV(cvv, data, oneStage);
956 });
957 $input.on('keypress', function(e) { if (e.which === 13) $confirm.trigger('click'); });
958 }
959
960 /**
961 * Re-calls retrieve_ticket with the CVV the customer typed.
962 * On success: either shows Stage 2 hand-back card (two-stage) or goes straight
963 * to the normal retrieved view (one-stage / one-stage option active).
964 */
965 function submitCVV(cvv, data, oneStage) {
966 updateTicketScannerInfoArea(_getSpinnerHTML());
967 _makeGet('retrieve_ticket', {'code': system.code, 'cvv': cvv}, function(resp) {
968 if (resp && resp.requires_cvv === true) {
969 if (resp.locked === true) {
970 system.status = 'locked';
971 showLockedScreen(resp);
972 return;
973 }
974 // Wrong CVV — re-prompt with updated counter
975 system.status = 'awaiting_cvv';
976 showCVVPrompt(resp);
977 return;
978 }
979 // CVV verified — store it for the downstream redeem call
980 system.currentCVV = cvv;
981 if (oneStage) {
982 renderRetrievedView(resp);
983 } else {
984 showHandBack(resp);
985 }
986 }, function(errResp) {
987 // Network or server error — show message and allow retry
988 clearAreas();
989 updateTicketScannerInfoArea('<h1 style="color:red !important;">' + (errResp && errResp.data ? errResp.data : __('Error', 'event-tickets-with-ticket-scanner')) + '</h1>');
990 showScanNextTicketButton();
991 });
992 }
993
994 /**
995 * Stage 2: hand-back card shown after CVV is verified in two-stage mode.
996 * Staff takes back the device and taps "Continue" to see the normal ticket view.
997 */
998 function showHandBack(resp) {
999 clearAreas();
1000 var $card = $('<div class="saso-cvv-handback">');
1001 $card.append('<h3 style="color:green;">✔ ' + __('Code valid — please take back the device', 'event-tickets-with-ticket-scanner') + '</h3>');
1002 var $cont = $('<button>', {'class': 'button button-primary'})
1003 .text(__('Continue →', 'event-tickets-with-ticket-scanner'));
1004 $cont.on('click', function() {
1005 renderRetrievedView(resp);
1006 });
1007 $card.append($cont);
1008 updateTicketScannerInfoArea($card);
1009 }
1010
1011 /**
1012 * Locked screen — shown when too many wrong CVV attempts have been made.
1013 * Displays only the public_ticket_id (no name, seat, or order data) to
1014 * prevent information leakage. Visual styling uses .saso-cvv-locked
1015 * (red border / warning background) so it is unmistakably distinct from
1016 * normal ticket-info screens. showScanNextTicketButton() provides the
1017 * "Back to scanner" path.
1018 */
1019 function showLockedScreen(data) {
1020 clearAreas();
1021 var $card = $('<div class="saso-cvv-locked">');
1022 $card.append('<h3 class="saso-cvv-locked__heading">🔒 ' + __('Ticket locked', 'event-tickets-with-ticket-scanner') + '</h3>');
1023 $card.append('<p>' + __('Too many wrong security code attempts. Please ask the event admin to reset this ticket.', 'event-tickets-with-ticket-scanner') + '</p>');
1024 if (data.public_ticket_id) {
1025 $card.append('<p>' + __('Ticket:', 'event-tickets-with-ticket-scanner') + ' <code>' + data.public_ticket_id + '</code></p>');
1026 }
1027 updateTicketScannerInfoArea($card);
1028 showScanNextTicketButton();
1029 }
1030
1031 /**
1032 * Transition into the normal "retrieved" view using a response payload that has
1033 * already passed the CVV gate. Mirrors the inline block inside retrieveTicket()
1034 * so the two-stage and one-stage CVV flows end up in exactly the same state as a
1035 * plain (non-CVV) retrieve.
1036 */
1037 function renderRetrievedView(data) {
1038 system.status = 'retrieved';
1039 system.data = data;
1040 system.last_scanned_ticket.data = data;
1041
1042 clearAreas();
1043
1044 if (typeof data.order_infos !== 'undefined' && data.order_infos.is_order_ticket) {
1045 system.format_datetime = data.option_displayDateTimeFormat;
1046 system.format_date = data.option_displayDateFormat;
1047 system.format_time = data.option_displayTimeFormat;
1048 displayOrderTicketInfo(data);
1049 showScanNextTicketButton();
1050 } else {
1051 system.format_datetime = data._ret.option_displayDateTimeFormat;
1052 system.format_date = data._ret.option_displayDateFormat;
1053 system.format_time = data._ret.option_displayTimeFormat;
1054 displayTicketInfo(data);
1055 displayTicketRetrievedInfo(data);
1056 displayTicketAdditionalInfos(data);
1057 $("#reader_output").html('');
1058 // Mirror the auto-redeem conditional from the inline retrieveTicket() block.
1059 // In the CVV path the ticket has not been redeemed before (redeemed=false equivalent),
1060 // so the only guard needed is redeem_auto + redeem_operation presence.
1061 if (ticket_scanner_operating_option.redeem_auto
1062 && typeof data._ret.redeem_operation !== 'undefined') {
1063 displayRedeemedInfo(system.code, data._ret.redeem_operation);
1064 } else {
1065 showScanNextTicketButton();
1066 }
1067 }
1068 }
1069
1070 // ── /CVV prompt ──────────────────────────────────────────────────────────
1071
1072 function isTicketExpired(ticketRetObject) {
1073 if (ticketRetObject.is_expired) return true;
1074 return false;
1075 }
1076 function isRedeemTooEarly(data) {
1077 if (data._ret._options.wcTicketDontAllowRedeemTicketBeforeStart) {
1078 return data._ret._options.isRedeemOperationTooEarly;
1079 }
1080 return false;
1081 }
1082 function isRedeemTooLate(data) {
1083 if (data._ret._options.wsticketDenyRedeemAfterstart) {
1084 return data._ret._options.isRedeemOperationTooLate;
1085 }
1086 return false;
1087 }
1088 function isRedeemTooLateEndEvent(data) {
1089 if (data._ret._options.wcTicketAllowRedeemTicketAfterEnd == false) {
1090 return data._ret._options.isRedeemOperationTooLateEventEnded;
1091 }
1092 return false;
1093 }
1094
1095 function canTicketBeRedeemedNow(data) {
1096 if (isRedeemTooEarly(data)) return false;
1097 if (isRedeemTooLateEndEvent(data)) return false;
1098 if (isRedeemTooLate(data)) return false;
1099 if (isTicketExpired(data._ret)) return false;
1100 return true;
1101 }
1102 function displayTicketRetrievedInfo(data) {
1103 let div = $('<div>').css("text-align", "center");
1104 let metaObj = data.metaObj
1105 let is_expired = isTicketExpired(data._ret);
1106 if (!data._ret.is_paid) {
1107 $('<h4 style="color:red !important;">').html(sprintf(/* translators: %s: order status */__('Ticket is NOT paid (%s).', 'label', 'event-tickets-with-ticket-scanner'),data._ret.order_status)).appendTo(div);
1108 } else {
1109 if (is_expired == false && metaObj['wc_ticket']['redeemed_date'] != "") {
1110 let color = "red";
1111 if (data._ret.max_redeem_amount > 1 && data.metaObj.wc_ticket.stats_redeemed.length < data._ret.max_redeem_amount) {
1112 color = "green";
1113 }
1114 if (data._ret.max_redeem_per_day > 0 && data._ret.redeems_today >= data._ret.max_redeem_per_day) {
1115 color = "red";
1116 }
1117 //if (system.last_scanned_ticket.auto_redeem == false) {
1118 if (system.redeemed_successfully) {
1119 $('<h4 style="color:'+color+' !important;">').html(data._ret.msg_redeemed).appendTo(div);
1120 }
1121 if (metaObj.wc_ticket.redeemed_date != '') {
1122 div.append(data._ret.redeemed_date_label+' '+metaObj['wc_ticket']['redeemed_date']);
1123 }
1124 } else {
1125 if (is_expired) {
1126 div.append('<div style="color:red;">'+data._ret.msg_ticket_expired+'</div>');
1127 div.append(data._ret.ticket_date_as_string);
1128 } else {
1129 if (data._ret.ticket_end_date == "" || data._ret.ticket_end_date_timestamp > time()) {
1130 div.append('<div style="color:green;">'+data._ret.msg_ticket_valid+'</div>');
1131 }
1132 }
1133 }
1134
1135 if (ticket_scanner_operating_option.distract_free) {
1136 // display ticket title and subtitle
1137 if (typeof data._ret.ticket_title != "undefined" && data._ret.ticket_title != "") {
1138 div.append('<h4>'+data._ret.ticket_title+'</h4>');
1139 }
1140 if (typeof data._ret.ticket_subtitle != "undefined" && data._ret.ticket_subtitle != "") {
1141 div.append('<h5>'+data._ret.ticket_subtitle+'</h5>');
1142 }
1143
1144 // display short description and ticket_info
1145 if (ticket_scanner_operating_option.distract_free_show_short_desc && typeof data._ret.short_desc != "undefined" && data._ret.short_desc != "") {
1146 div.append('<div>'+data._ret.short_desc+'</div>');
1147 }
1148 if (typeof data._ret.ticket_info != "undefined" && data._ret.ticket_info != "") {
1149 //div.append('<div>'+data._ret.ticket_info+'</div>');
1150 }
1151 //console.log(data._ret);
1152 }
1153 if (is_expired == false) {
1154 let _isRedeemTooLate = isRedeemTooLate(data);
1155 let _isRedeemTooLateEndEvent = isRedeemTooLateEndEvent(data);
1156 if (!canTicketBeRedeemedNow(data)) {
1157 let error_msg = data._ret.msg_ticket_not_valid_yet;
1158 if(_isRedeemTooLateEndEvent) {
1159 error_msg = data._ret.msg_ticket_event_ended;
1160 } else if(_isRedeemTooLate) {
1161 error_msg = data._ret.msg_ticket_not_valid_anymore;
1162 }
1163 div.append('<div style="color:red;">'+error_msg+'</div>');
1164 }
1165 if (_isRedeemTooLate == false && _isRedeemTooLateEndEvent == false && data._ret._options.wcTicketDontAllowRedeemTicketBeforeStart) {
1166 if (typeof data._ret.redeem_allowed_from != "undefined" && typeof data._ret.is_date_set != "undefined" && data._ret.is_date_set) {
1167 //div.append("<div>Redeem allowed from: <b>"+data._ret.redeem_allowed_from+"</b></div>");
1168 div.append('<div style="display: flex;flex-wrap: wrap;flex-direction: column;"><div>Redeem allowed from: </div><div style="font-weight:bold;">'+data._ret.redeem_allowed_from+'</div></div>');
1169 }
1170 }
1171 }
1172 }
1173 $('#ticket_info_retrieved').html(div);
1174 }
1175 function displayTicketAdditionalInfos(data) {
1176 let div = $('<div style="width:50%;display:inline-block;">');
1177 if (data._ret.is_paid) {
1178 $('<div>').html('<b>'+__('Ticket paid', 'event-tickets-with-ticket-scanner')+'</b>').css("color", "green").appendTo(div);
1179 } else {
1180 $('<div>').html(__('Ticket NOT paid', 'event-tickets-with-ticket-scanner')).css("color", "red").appendTo(div);
1181 }
1182 // Seat information
1183 let seatHtml = _getSeatInfoHtml(data._ret);
1184 if (seatHtml != '') {
1185 $('<div>').html(seatHtml).appendTo(div);
1186 }
1187 if (data.metaObj.wc_ticket.redeemed_date != "") {
1188 $('<div>').html(__('Ticket is already redeemed', 'event-tickets-with-ticket-scanner')).appendTo(div);
1189 } else {
1190 $('<div>').html(__('Ticket not redeemed', 'event-tickets-with-ticket-scanner')).appendTo(div);
1191 }
1192 if (data._ret._options.displayConfirmedCounter) {
1193 $('<div>').html(sprintf(/* translators: %s: confirmed check counter */__('Confirmed status validation check counter: <b>%s</b>', 'event-tickets-with-ticket-scanner'), data.metaObj.confirmedCount)).appendTo(div);
1194 }
1195 $('<div>').html(sprintf(/* translators: %s: max redeem amount */__('Max Redeem Amount for this ticket: <b>%s</b>', 'event-tickets-with-ticket-scanner'), data._ret.max_redeem_amount)).appendTo(div);
1196 if(data._ret.max_redeem_amount > 1) {
1197 $('<div>').html(sprintf(/* translators: 1: redeemd tickets 2: max redeem */__('Redeem usage: <b>%1$d</b> of <b>%2$d</b>', 'event-tickets-with-ticket-scanner'), data.metaObj.wc_ticket.stats_redeemed.length, data._ret.max_redeem_amount)).appendTo(div);
1198 }
1199 if (data._ret.max_redeem_per_day > 0) {
1200 $('<div>').html(sprintf(/* translators: 1: redeems used today 2: max per day */__('Redeems today: <b>%1$d</b> of <b>%2$d</b>', 'event-tickets-with-ticket-scanner'), data._ret.redeems_today, data._ret.max_redeem_per_day)).appendTo(div);
1201 }
1202
1203 let div2 = $('<div style="width:50%;display:inline-block;">');
1204 if (data._ret._options.wcTicketDontAllowRedeemTicketBeforeStart && typeof data._ret.is_date_set != "undefined" && data._ret.is_date_set) {
1205 //if (data._ret._options.isRedeemOperationTooEarly) {
1206 div2.append($('<div>').html(sprintf(/* translators: %s: date */__('Redeemable from %s', 'event-tickets-with-ticket-scanner'), data._ret.redeem_allowed_from)));
1207 //}
1208 }
1209 if (typeof data._ret.redeem_allowed_until != "undefined" && typeof data._ret.is_date_set != "undefined" && data._ret.is_date_set) {
1210 div2.append($('<div>').html(sprintf(/* translators: %s: date */__('Redeemable until %s', 'event-tickets-with-ticket-scanner'), data._ret.redeem_allowed_until)));
1211 }
1212
1213 if (data.metaObj.woocommerce.creation_date != "") {
1214 div2.append('<div>'+sprintf(/* translators: %s: date */__('Bought at %s', 'event-tickets-with-ticket-scanner'), DateTime2Text(new Date(data.metaObj.woocommerce.creation_date).getTime()))+'</div>');
1215 }
1216
1217 let is_expired = isTicketExpired(data._ret);
1218 if (typeof data.metaObj.expiration != "undefined") {
1219 if (data.metaObj.expiration.date != "") {
1220 div2.append('<div'+(is_expired ? ' style="font-weight:bold;"' : '')+'>'+sprintf(/* translators: %s: date */__('Expiration at %s', 'event-tickets-with-ticket-scanner'), DateTime2Text(new Date(data.metaObj.expiration.date).getTime()))+'</div>');
1221 } else {
1222 let date_expiration_ms = new Date(data.metaObj.woocommerce.creation_date).getTime();
1223 date_expiration_ms += data.metaObj.expiration.days * 24 * 3600 * 1000;
1224 let exp_text = data.metaObj.expiration.days > 0 ? sprintf(/* translators: 1: days 2: date */__('Expires after %1$d days (%2$s)', 'event-tickets-with-ticket-scanner'), data.metaObj.expiration.days, DateTime2Text( date_expiration_ms )) : '';
1225 if (exp_text != "") {
1226 div2.append('<div>'+exp_text+'</div>');
1227 }
1228 }
1229 }
1230
1231 let div3 = $('<div>');
1232 if (typeof data._ret.product !== "undefined") {
1233 let product_name = data._ret.product.name + (data._ret.product.name_variant != "" ? " - "+data._ret.product.name_variant : "");
1234 div3.css("margin-top", "10px").html(__('<b>Product information</b>', 'event-tickets-with-ticket-scanner'))
1235 .append('<div>'+sprintf(__('#%s - %s', 'event-tickets-with-ticket-scanner'), data._ret.product.id, product_name)+'</div>');
1236 if (data._ret.product.sku != "") {
1237 div3.append('<div>'+sprintf(__('SKU: %s', 'event-tickets-with-ticket-scanner'), data._ret.product.sku)+'</div>');
1238 }
1239 }
1240 let content = "";
1241 if (ticket_scanner_operating_option.distract_free) {
1242 content = '<div style="display:flex;text-align:center;flex-wrap: nowrap;flex-direction: row;justify-content: center;flex-basis: auto;">'+system.code+'</div>';
1243 }
1244 $('#ticket_add_info').html(content)
1245 .append( $('<div style="padding-top:10px;width:100%;">').append(div).append(div2) )
1246 .append(div3);
1247 }
1248 function displayRedeemedInfo(code, data) {
1249 system.status = "redeemed";
1250 system.redeemed_successfully = data.redeem_successfully;
1251 vibrateOnResult(data.redeem_successfully);
1252 displayTicketRedeemedInfo(data);
1253 if(ticket_scanner_operating_option.redeem_auto) {
1254 showScanNextTicketButton();
1255 } else {
1256 //retrieveTicket(code, true);
1257 }
1258 system.INPUTFIELD.focus();
1259 system.INPUTFIELD.select();
1260 }
1261 function displayTicketRedeemedInfo(data) {
1262 let t = '';
1263 // zeige retrieved info an
1264 let content = $('<div>').html('<div style="display:flex;text-align:center;flex-wrap: nowrap;flex-direction: row;justify-content: center;flex-basis: auto;">'+system.code+'</div>');
1265 if (system.redeemed_successfully) {
1266 content.append('<h3 style="color:green !important;text-align:center;">'+__('Redeemed', 'event-tickets-with-ticket-scanner')+'</h3>');
1267 //content.append('<p style="text-align:center;color:green"><img src="'+system.img_pfad+'button_ok.png"><br><b>'+__('Successfully redeemed', 'event-tickets-with-ticket-scanner')+'</b></p>');
1268 content.append('<p style="text-align:center;color:green"><img src="'+system.img_pfad+'button_ok.png"></p>');
1269 t = 'Redeemed';
1270 } else {
1271 content.append('<h3 style="color:red !important;text-align:center;">'+__('NOT REDEEMED - see reason below', 'event-tickets-with-ticket-scanner')+'</h3>');
1272 //content.append('<p style="text-align:center;color:red;"><img src="'+system.img_pfad+'button_cancel.png"><br><b>'+__('Failed to redeem', 'event-tickets-with-ticket-scanner')+'</b></p>');
1273 content.append('<p style="text-align:center;color:red;"><img src="'+system.img_pfad+'button_cancel.png"></p>');
1274 t = 'Not redeemed';
1275 }
1276 if (typeof system.last_scanned_ticket.data != null && system.last_scanned_ticket.data._ret && system.last_scanned_ticket.data._ret.ticket_title && system.last_scanned_ticket.data._ret.ticket_title != "") {
1277 content.append('<h4 style="text-align:center;">'+system.last_scanned_ticket.data._ret.ticket_title+'</h4>');
1278 }
1279 if (typeof system.last_scanned_ticket.data._ret.ticket_subtitle != "undefined" && system.last_scanned_ticket.data._ret && system.last_scanned_ticket.data._ret.ticket_subtitle && system.last_scanned_ticket.data._ret.ticket_subtitle != "") {
1280 content.append('<h5 style="text-align:center;">'+system.last_scanned_ticket.data._ret.ticket_subtitle+'</h5>');
1281 }
1282 if (ticket_scanner_operating_option.distract_free_show_short_desc && typeof system.last_scanned_ticket.data._ret.short_desc != "undefined" && system.last_scanned_ticket.data._ret.short_desc != "") {
1283 content.append('<div>'+system.last_scanned_ticket.data._ret.short_desc+'</div>');
1284 }
1285
1286 if (typeof system.last_scanned_ticket.data != null && system.last_scanned_ticket.data.metaObj && system.last_scanned_ticket.data.metaObj.wc_ticket && system.last_scanned_ticket.data.metaObj.wc_ticket.redeemed_date && system.last_scanned_ticket.data.metaObj.wc_ticket.redeemed_date != "") {
1287 content.append('<div style="text-align:center;">'+system.last_scanned_ticket.data._ret.redeemed_date_label+' '+system.last_scanned_ticket.data.metaObj.wc_ticket.redeemed_date+'</div>');
1288 }
1289 speakText(t, 'en-EN');
1290 showScanNextTicketButton();
1291 updateTicketScannerInfoArea(content);
1292 }
1293 function displayRedeemedOrderInfo(code, data) {
1294 vibrateOnResult(data.errors.length === 0);
1295 let content = $('<div>');
1296 content.html('<center>'+code+'</center>');
1297 if (data.errors.length > 0) {
1298 content.append('<h3 style="color:red !important;text-align:center;">'+__('ERRORS - see reason below', 'event-tickets-with-ticket-scanner')+'</h3>');
1299 } else if (data.not_redeemed.length) { // is not implemented yet
1300 content.append('<h3 style="color:orange !important;text-align:center;">'+__('NOT REDEEMED - see reason below', 'event-tickets-with-ticket-scanner')+'</h3>');
1301 } else {
1302 content.append('<h3 style="color:green !important;text-align:center;">'+__('Order Redeemed', 'event-tickets-with-ticket-scanner')+'</h3>');
1303 }
1304 updateTicketScannerInfoArea(content);
1305
1306 system.INPUTFIELD.focus();
1307 system.INPUTFIELD.select();
1308 }
1309 function redeemTicket(code) {
1310 clearAreas();
1311 system.redeemed_successfully = false;
1312 $("#reader_output").html(__("start redeem ticket...loading..."));
1313 updateTicketScannerInfoArea(_getSpinnerHTML());
1314 _makeGet('redeem_ticket', {'code':code, 'cvv': system.currentCVV || ''}, data=>{
1315 system.data = data;
1316 $("#reader_output").html('');
1317
1318 if (typeof data.is_order_ticket !== "undefined" && data.is_order_ticket) {
1319 // update li
1320 data.errors.forEach(item=>{
1321 let elems = $('#order_info').find('li[data-id="'+encodeURIComponent(item.code)+'"]');
1322 elems.css("padding", "5px");
1323 elems.css("margin-bottom", "5px");
1324 elems.css("background-color", "red");
1325 //elems.css("color", "white");
1326 elems.append("<br>"+item.error);
1327 });
1328 data.not_redeemed.forEach(item=>{
1329 let elems = $('#order_info').find('li[data-id="'+encodeURIComponent(item.code)+'"]');
1330 elems.css("padding", "5px");
1331 elems.css("margin-bottom", "5px");
1332 elems.css("background-color", "orange");
1333 elems.css("color", "black");
1334 elems.append("<br>Not redeemed");
1335 });
1336 data.redeemed.forEach(item=>{
1337 let info = item._ret.tickets_redeemed_show ? "<br>Redeemed: "+item._ret.tickets_redeemed:'';
1338 let elems = $('#order_info').find('li[data-id="'+encodeURIComponent(item.code)+'"]');
1339 elems.css("padding", "5px");
1340 elems.css("margin-bottom", "5px");
1341 elems.css("background-color", "green");
1342 //elems.css("color", "white");
1343 elems.append(info);
1344 });
1345 displayRedeemedOrderInfo(code, data);
1346 } else {
1347 displayRedeemedInfo(code, data);
1348 $('#ticket_info_btns').append(displayRedeemedTicketsInfo(data));
1349 }
1350
1351 }, response=>{
1352 clearAreas();
1353 $("#reader_output").html('');
1354 updateTicketScannerInfoArea('<h1 style="color:red !important;">'+response.data+'</h3>');
1355
1356 showScanNextTicketButton();
1357
1358 system.INPUTFIELD.focus();
1359 system.INPUTFIELD.select();
1360 });
1361 }
1362 function displayOrderTicketInfo(data) {
1363 let div = $('<div>').css('padding', '10px');
1364
1365 div.html('<h3 style="text-align:center;color:black !important;">'+_x("Order Ticket", 'label', 'event-tickets-with-ticket-scanner')+'</h3>');
1366 div.append($('<div style="text-align:center;">').html(data.order_infos.code));
1367 div.append("<b>"+_x("Includes", 'label', 'event-tickets-with-ticket-scanner')+": </b>"+sprintf(__('%s Products, %s Tickets', 'event-tickets-with-ticket-scanner'), data.order_infos.products.length, data.ticket_infos.length)+'<br>');
1368 div.append('<b>'+_x("Order ID", 'label', 'event-tickets-with-ticket-scanner')+': </b>#'+data.order_infos.id+'<br>');
1369 div.append('<b>'+_x("Created", 'label', 'event-tickets-with-ticket-scanner')+': </b>'+data.order_infos.date_created+'<br>');
1370 div.append('<b>'+_x("Completed", 'label', 'event-tickets-with-ticket-scanner')+': </b>'+data.order_infos.date_completed+'<br>');
1371 div.append('<b>'+_x("Paid", 'label', 'event-tickets-with-ticket-scanner')+': </b>'+data.order_infos.date_paid+'<br>');
1372 div.append($('<button class="button-ticket-options button-primary">').html(_x("Redeem Complete Order", 'label', 'event-tickets-with-ticket-scanner')).on("click", e=>{
1373 redeemTicket(data.order_infos.code);
1374 }));
1375
1376 // Task 15 — per-row CVV inputs. If any ticket row requires CVV, render a
1377 // CVV input next to it and a single "Confirm codes" button below the list
1378 // that re-calls retrieve_order_ticket with the per-row CVV map.
1379 let anyCvvRequired = false;
1380 for (let i = 0; i < data.ticket_infos.length; i++) {
1381 if (data.ticket_infos[i] && data.ticket_infos[i].requires_cvv === true) {
1382 anyCvvRequired = true;
1383 break;
1384 }
1385 }
1386
1387 let div_tickets = $('<div style="padding-top:15px;text-align:left;">');
1388 for (let pidx=0;pidx<data.order_infos.products.length;pidx++) {
1389 let product = data.order_infos.products[pidx];
1390 div_tickets.append("<b>"+product.product_name
1391 +(product.product_name_variant != "" ? " - "
1392 +product.product_name_variant : "")
1393 +'</b>');
1394 let ol = $('<ol style="padding-top:5px;">');
1395 for (let idx=0;idx<data.ticket_infos.length;idx++) {
1396 let item = data.ticket_infos[idx];
1397 if (item.product_id == product.product_id && item.product_parent_id == product.product_parent_id) {
1398 let li = $('<li data-id="'+encodeURIComponent(item.code_display || item.code_public || '')+'" style="padding-bottom:10px;">');
1399
1400 if (item.requires_cvv === true) {
1401 // Render minimal locked/CVV-required UI per row.
1402 let lockedRow = item.locked === true;
1403 let codeKey = item.code || item.code_display || item.code_public || '';
1404 let displayKey = item.code_display || item.code_public || '';
1405 let attemptsLeft = parseInt(item.attempts_remaining, 10);
1406 if (isNaN(attemptsLeft)) attemptsLeft = 5;
1407
1408 let header = '<b>'+(displayKey || _x('Ticket', 'label', 'event-tickets-with-ticket-scanner'))+'</b><br>';
1409 if (lockedRow) {
1410 header += '<span style="color:red;">🔒 '+__('Ticket locked', 'event-tickets-with-ticket-scanner')+'</span>';
1411 li.append(header);
1412 } else {
1413 header += '<span style="color:#888;">🔒 '+__('Security code required', 'event-tickets-with-ticket-scanner')+'</span><br>';
1414 let $cvvInput = $('<input>', {
1415 type: 'text',
1416 maxlength: 4,
1417 'class': 'saso-cvv-input saso-cvv-row-input',
1418 autocomplete: 'off',
1419 autocapitalize: 'characters',
1420 inputmode: 'text',
1421 placeholder: 'CVV',
1422 'data-code': codeKey
1423 }).css({'width': '80px', 'margin-right': '6px'});
1424 let $hint = $('<small>').css('color', '#888').text(
1425 ' '+__('Attempts remaining:', 'event-tickets-with-ticket-scanner')+' '+attemptsLeft
1426 );
1427 li.append(header).append($cvvInput).append($hint);
1428 }
1429 } else {
1430 let extra_content = (item.code_display || '') + '<br>';
1431 if (item.name_per_ticket != "" || item.value_per_ticket != "") {
1432 extra_content += item.name_per_ticket+" "+item.value_per_ticket;
1433 } else {
1434 extra_content += "No name or value on ticket set";
1435 }
1436 // Seat information
1437 let itemSeatHtml = _getSeatInfoHtml(item);
1438 if (itemSeatHtml != '') {
1439 extra_content += "<br>" + itemSeatHtml;
1440 }
1441 if (item.location) {
1442 extra_content += "<br>"+item.location;
1443 }
1444 if (item.ticket_date) {
1445 extra_content += "<br>"+item.ticket_date;
1446 }
1447 li.append(extra_content+'<br>')
1448 .append($('<button style="color:white;border-color:#008CBA;background-color:#008CBA;">').html("Retrieve ticket").on("click", e=>{
1449 retrieveTicket(item.code_public, true); // do not redeem automatically
1450 }))
1451 .append($('<button style="color:white;border-color:red;background-color:red;">').html("Redeem ticket").on("click", e=>{
1452 redeemTicket(item.code_public);
1453 }));
1454 }
1455 li.appendTo(ol);
1456 }
1457 }
1458 ol.appendTo(div_tickets);
1459 }
1460 div_tickets.appendTo(div);
1461
1462 if (anyCvvRequired) {
1463 let $confirmBtn = $('<button class="button-ticket-options button-primary">')
1464 .html(_x("Confirm codes", 'label', 'event-tickets-with-ticket-scanner'))
1465 .css({'margin-top': '10px'})
1466 .on('click', function() {
1467 let cvvMap = {};
1468 $('.saso-cvv-row-input', div).each(function() {
1469 let $i = $(this);
1470 let v = ($i.val() || '').trim();
1471 let k = $i.attr('data-code') || '';
1472 if (k !== '' && v !== '') cvvMap[k] = v;
1473 });
1474 // Re-call retrieve_order_ticket with the per-row map.
1475 _makeGet('retrieve_ticket', {'code': data.order_infos.code, 'cvv': cvvMap}, function(resp) {
1476 if (resp && resp.order_infos && resp.order_infos.is_order_ticket) {
1477 displayOrderTicketInfo(resp);
1478 }
1479 });
1480 });
1481 div.append($confirmBtn);
1482 }
1483
1484 div_order_info_area = $('#order_info').html(div);
1485 }
1486 function displayTicketInfo(data) {
1487 let codeObj = data;
1488 let metaObj = data.metaObj;
1489 let ret = data._ret;
1490 let div = $('<div>').css('padding', '10px');
1491 let border_color = 'green';
1492 if (isTicketExpired(data._ret)) {
1493 border_color = 'orange';
1494 }
1495 if (metaObj['wc_ticket']['redeemed_date'] != "") {
1496 border_color = 'red';
1497 }
1498 div.css("border", "1px solid "+border_color);
1499
1500 $('<h3 style="color:black !important;text-align:center;">').html(ret.ticket_heading).appendTo(div);
1501 $('<h4 style="color:black !important;margin-bottom:0;">').html(ret.ticket_title).appendTo(div);
1502 /* // ?? is the same like ret.ticiet_sub_title
1503 if (data._ret.product.name_variant != "") {
1504 $('<h5 style="color:black !important;margin-top:0;padding-top:0;">').html(data._ret.product.name_variant).appendTo(div);
1505 }
1506 */
1507 if (ret.ticket_sub_title != "") {
1508 $('<h5 style="color:black !important;margin-top:0;padding-top:0;">').html(ret.ticket_sub_title).appendTo(div);
1509 }
1510 $('<p>').html(ret.ticket_date_as_string).appendTo(div);
1511 if (ret.ticket_location != "") {
1512 $('<p>').html(ret.ticket_location_label+' '+ret.ticket_location).appendTo(div);
1513 }
1514 // Seat information - display prominently after location
1515 let seatHtml = _getSeatInfoHtml(ret);
1516 if (seatHtml != '') {
1517 $('<p>').html(seatHtml).appendTo(div);
1518 }
1519 if (ret.short_desc != "") {
1520 div.append(ret.short_desc).append('<br>');
1521 }
1522 if (ret.ticket_info != "") {
1523 $('<p>').html(ret.ticket_info).appendTo(div);
1524 }
1525 if (ret.cst_label != "") {
1526 $('<p>').html('<b>'+ret.cst_label+'</b><br>'+ret.cst_billing_address+'<br>').appendTo(div);
1527 }
1528 if (ret.payment_label != "") {
1529 let date_order_paid = ret.payment_paid_at;
1530 let date_order_complete = null;
1531 if (ret.payment_completed_at !== "undefined") {
1532 date_order_complete = ret.payment_completed_at;
1533 }
1534 let p = $('<p>').appendTo(div);
1535 p.append('<b>'+ret.payment_label+'</b><br>');
1536 p.append("Order status: "+ret.order_status+"<br>");
1537 p.append(ret.payment_paid_at_label+' ');
1538 p.append('<b>'+date_order_paid+'</b><br>');
1539 if (date_order_complete != null) {
1540 p.append(ret.payment_completed_at_label+' ');
1541 p.append('<b>'+date_order_complete+'</b><br>');
1542 }
1543 p.append(ret.payment_method_label);
1544 if (ret.payment_method != "") {
1545 p.append(' '+ret.payment_method+' '+ret.payment_trx_id);
1546 }
1547 p.append('<br>');
1548 if (ret.coupon != "") {
1549 p.append(ret.coupon_label+' <b>'+ret.coupon+'</b><br>');
1550 }
1551 }
1552 if (metaObj.wc_ticket.name_per_ticket != "") {
1553 $('<p>').html(ret.name_per_ticket_label + " " +metaObj.wc_ticket.name_per_ticket).appendTo(div);
1554 }
1555 if (metaObj.wc_ticket.value_per_ticket != "") {
1556 $('<p>').html(ret.value_per_ticket_label + " " +metaObj.wc_ticket.value_per_ticket).appendTo(div);
1557 }
1558 if (ret.ticket_amount_label != "") {
1559 $('<p>').html(ret.ticket_amount_label).appendTo(div);
1560 }
1561 let p = $('<p>').html(ret.ticket_label+' <b>'+codeObj['code_display']+'</b><br>').appendTo(div);
1562 p.append(ret.paid_price_label+' <b>'+ret.paid_price_as_string+'</b>');
1563 if (ret.product_price != ret.paid_price) {
1564 p.append(' <b>('+ret.product_price_label+' '+ret.product_price_as_string+')</b>');
1565 }
1566 $('<p style="text-align:center;">').html(system.code).appendTo(div);
1567
1568 div_ticket_info_area = $('#ticket_info').html(div);
1569 displayTicketInfoButtons(data);
1570 }
1571 function canTicketBeRedeemed(data) {
1572 let allow_redeem = false;
1573 if (data._ret.allow_redeem_only_paid) {
1574 if (data._ret.is_paid) {
1575 allow_redeem = true;
1576 }
1577 } else {
1578 allow_redeem = true;
1579 }
1580 if (allow_redeem) {
1581 if (data.metaObj['wc_ticket']['redeemed_date'] != "") {
1582 allow_redeem = false;
1583 }
1584 if (data._ret.max_redeem_amount > 1 && data.metaObj.wc_ticket.stats_redeemed.length < data._ret.max_redeem_amount) {
1585 allow_redeem = true;
1586 }
1587 if (data._ret.max_redeem_per_day > 0 && data._ret.redeems_today >= data._ret.max_redeem_per_day) {
1588 allow_redeem = false;
1589 }
1590 if (allow_redeem) {
1591 allow_redeem = canTicketBeRedeemedNow(data);
1592 }
1593 }
1594 return allow_redeem;
1595 }
1596 function displayTicketInfoButtons(data) {
1597 let div = $('<div>').css('text-align', 'center');
1598 if (!data._ret.is_paid) {
1599 $('<h4 style="color:red !important;">').html(sprintf(/* translators: %s: order status */__('Ticket is NOT paid (%s).', 'event-tickets-with-ticket-scanner'), data._ret.order_status)).appendTo(div);
1600 }
1601 $('<button class="button-ticket-options">').html(_x('Reload', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div).on('click', e=>{
1602 retrieveTicket(system.code, true);
1603 });
1604 let btn_redeem = $('<button class="button-ticket-options">').html(_x('Redeem Ticket', 'label', 'event-tickets-with-ticket-scanner')).css("background-color", 'gray').css('color', 'white').prop("disabled", true).appendTo(div).on('click', e=>{
1605 redeemTicket(system.code);
1606 });
1607 if (ticket_scanner_operating_option.ticketScannerDontShowBtnPDF == false) {
1608 $('<button class="button-ticket-options">').html(_x('PDF', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div).on('click', e=>{
1609 window.open(data.metaObj['wc_ticket']['_url']+'?pdf', '_blank');
1610 });
1611 }
1612 if (ticket_scanner_operating_option.ticketScannerDontShowBtnBadge == false) {
1613 $('<button class="button-ticket-options">').html(_x('Badge', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div).on('click', e=>{
1614 _downloadFile('downloadPDFTicketBadge', {'code':data.code}, "eventticket_badge_"+data.code+".pdf");
1615 return false;
1616 });
1617 }
1618 // Venue Image button - show if venue image exists (for all plan types)
1619 if (data._ret.seating_plan_show_venue_button && data._ret.seat_id > 0) {
1620 $('<button class="button-ticket-options btn-venue-image">').html(_x('Venue Image', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div).on('click', e=>{
1621 showVenueImageModal(data);
1622 return false;
1623 });
1624 }
1625 // Visual Seating Plan button - show only for visual plans (lazy loaded)
1626 if (data._ret.seating_plan_show_visual_button && data._ret.seat_id > 0) {
1627 $('<button class="button-ticket-options btn-seating-plan">').html(_x('Seating Plan', 'label', 'event-tickets-with-ticket-scanner')).appendTo(div).on('click', e=>{
1628 loadAndShowSeatingPlan(data._ret.seating_plan_id, data._ret.seat_id, data._ret);
1629 return false;
1630 });
1631 }
1632
1633 if (canTicketBeRedeemed(data)) {
1634 btn_redeem.prop("disabled", false).css('background-color','green');
1635 }
1636
1637 div.append(displayRedeemedTicketsInfo(data));
1638 div.append(displayTimezoneInformation(data));
1639 $('#ticket_info_btns').html(div);
1640 }
1641 // Helper: escape HTML
1642 function escapeHtml(text) {
1643 if (!text) return '';
1644 return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1645 }
1646
1647 // Build SVG seating map (same approach as seating_frontend.js)
1648 function buildSeatingPlanSvg(planData, currentSeatId) {
1649 let meta = planData.meta || {};
1650 let width = meta.canvas_width || 800;
1651 let height = meta.canvas_height || 600;
1652 let bgColor = meta.background_color || '#ffffff';
1653 let bgImage = meta.background_image || planData.planImage || '';
1654
1655 let svg = '<svg class="saso-seat-map-readonly" viewBox="0 0 ' + width + ' ' + height + '" style="background-color: ' + bgColor + ';">';
1656
1657 // Background image
1658 if (bgImage) {
1659 svg += '<image href="' + escapeHtml(bgImage) + '" x="0" y="0" width="' + width + '" height="' + height + '" preserveAspectRatio="xMidYMid meet" />';
1660 }
1661
1662 // Decorations layer
1663 (meta.decorations || []).forEach(function(el) {
1664 svg += buildSvgElement(el);
1665 });
1666
1667 // Lines layer
1668 (meta.lines || []).forEach(function(el) {
1669 svg += buildSvgElement(el);
1670 });
1671
1672 // Labels layer
1673 (meta.labels || []).forEach(function(el) {
1674 svg += buildSvgElement(el);
1675 });
1676
1677 // Seats layer
1678 (planData.seats || []).forEach(function(seat) {
1679 svg += buildSeatSvgElement(seat, currentSeatId);
1680 });
1681
1682 svg += '</svg>';
1683 return svg;
1684 }
1685
1686 // Build SVG element (decoration, line, label)
1687 function buildSvgElement(el) {
1688 let type = el.type || 'rect';
1689 let x = parseFloat(el.x) || 0;
1690 let y = parseFloat(el.y) || 0;
1691 let fill = el.fill || 'transparent';
1692 let stroke = el.stroke || 'none';
1693 let strokeWidth = el.strokeWidth || 1;
1694 let fillOpacity = el.fillOpacity !== undefined ? (parseFloat(el.fillOpacity) / 100) : 1;
1695 let strokeOpacity = el.strokeOpacity !== undefined ? (parseFloat(el.strokeOpacity) / 100) : 0;
1696
1697 let svgEl = '';
1698 switch (type) {
1699 case 'rect':
1700 let rw = parseFloat(el.width) || 50;
1701 let rh = parseFloat(el.height) || 50;
1702 let rx = el.rx || 0;
1703 svgEl = '<rect x="' + x + '" y="' + y + '" width="' + rw + '" height="' + rh + '" rx="' + rx + '" fill="' + fill + '" fill-opacity="' + fillOpacity + '" stroke="' + stroke + '" stroke-opacity="' + strokeOpacity + '" stroke-width="' + (strokeOpacity > 0 ? strokeWidth : 0) + '" />';
1704 break;
1705 case 'circle':
1706 let r = parseFloat(el.r) || 25;
1707 let cx = x + r;
1708 let cy = y + r;
1709 svgEl = '<circle cx="' + cx + '" cy="' + cy + '" r="' + r + '" fill="' + fill + '" fill-opacity="' + fillOpacity + '" stroke="' + stroke + '" stroke-opacity="' + strokeOpacity + '" stroke-width="' + (strokeOpacity > 0 ? strokeWidth : 0) + '" />';
1710 break;
1711 case 'line':
1712 let x1 = parseFloat(el.x1) || 0;
1713 let y1 = parseFloat(el.y1) || 0;
1714 let x2 = parseFloat(el.x2) || 100;
1715 let y2 = parseFloat(el.y2) || 100;
1716 let lineOpacity = el.strokeOpacity !== undefined ? (parseFloat(el.strokeOpacity) / 100) : 1;
1717 svgEl = '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '" stroke="' + stroke + '" stroke-opacity="' + lineOpacity + '" stroke-width="' + strokeWidth + '" />';
1718 break;
1719 case 'text':
1720 let fontSize = el.fontSize || 14;
1721 svgEl = '<text x="' + x + '" y="' + y + '" fill="' + fill + '" fill-opacity="' + fillOpacity + '" font-size="' + fontSize + '" font-family="sans-serif">' + escapeHtml(el.text || '') + '</text>';
1722 break;
1723 case 'image':
1724 let iw = el.width || 100;
1725 let ih = el.height || 100;
1726 svgEl = '<image href="' + escapeHtml(el.href || '') + '" x="' + x + '" y="' + y + '" width="' + iw + '" height="' + ih + '" opacity="' + fillOpacity + '" />';
1727 break;
1728 }
1729 return svgEl;
1730 }
1731
1732 // Build seat SVG element
1733 function buildSeatSvgElement(seat, currentSeatId) {
1734 let meta = seat.meta || {};
1735 let posX = parseFloat(meta.pos_x) || parseFloat(meta.x) || 0;
1736 let posY = parseFloat(meta.pos_y) || parseFloat(meta.y) || 0;
1737 let shapeConfig = meta.shape_config || {width: 30, height: 30};
1738 let shapeType = meta.shape_type || meta.shape || 'rect';
1739 let seatWidth = parseFloat(shapeConfig.width) || parseFloat(meta.width) || 30;
1740 let seatHeight = parseFloat(shapeConfig.height) || parseFloat(meta.height) || 30;
1741 let seatLabel = meta.seat_label || seat.seat_identifier || '';
1742 let seatColor = meta.color || '#4CAF50';
1743
1744 let isCurrent = seat.is_current || (String(seat.id) === String(currentSeatId));
1745 let fillColor = isCurrent ? '#4CAF50' : (seatColor || '#cccccc');
1746 let opacity = isCurrent ? '1' : '0.4';
1747 let strokeColor = isCurrent ? '#ff0000' : 'transparent';
1748 let strokeWidth = isCurrent ? '4' : '0';
1749
1750 let svgEl = '';
1751 let textX, textY;
1752
1753 if (shapeType === 'circle') {
1754 let r = seatWidth / 2;
1755 let cx = posX + r;
1756 let cy = posY + r;
1757 textX = cx;
1758 textY = cy;
1759 svgEl = '<circle cx="' + cx + '" cy="' + cy + '" r="' + r + '" fill="' + fillColor + '" opacity="' + opacity + '" stroke="' + strokeColor + '" stroke-width="' + strokeWidth + '"' + (isCurrent ? ' class="current-seat"' : '') + ' />';
1760 } else {
1761 textX = posX + seatWidth / 2;
1762 textY = posY + seatHeight / 2;
1763 svgEl = '<rect x="' + posX + '" y="' + posY + '" width="' + seatWidth + '" height="' + seatHeight + '" rx="3" fill="' + fillColor + '" opacity="' + opacity + '" stroke="' + strokeColor + '" stroke-width="' + strokeWidth + '"' + (isCurrent ? ' class="current-seat"' : '') + ' />';
1764 }
1765
1766 // Seat label
1767 let labelSize = Math.min(seatWidth, seatHeight) * 0.35;
1768 svgEl += '<text x="' + textX + '" y="' + textY + '" text-anchor="middle" dominant-baseline="middle" fill="' + (isCurrent ? '#fff' : '#333') + '" font-size="' + labelSize + '" font-weight="bold" pointer-events="none">' + escapeHtml(seatLabel) + '</text>';
1769
1770 return svgEl;
1771 }
1772
1773 function showVenueImageModal(data) {
1774 let ret = data._ret;
1775 // Create modal overlay
1776 let overlay = $('<div class="seating-plan-modal-overlay">').on('click', function(e) {
1777 if (e.target === this) {
1778 $(this).remove();
1779 }
1780 });
1781 let modal = $('<div class="seating-plan-modal">');
1782
1783 // Header with close button
1784 let header = $('<div class="seating-plan-modal-header">');
1785 $('<h3>').text(ret.seating_plan_name || _x('Venue', 'label', 'event-tickets-with-ticket-scanner')).appendTo(header);
1786 $('<button class="seating-plan-modal-close">&times;</button>').on('click', function() {
1787 overlay.remove();
1788 }).appendTo(header);
1789 modal.append(header);
1790
1791 // Content area
1792 let content = $('<div class="seating-plan-modal-content">');
1793
1794 // Seat info banner
1795 let seatBanner = $('<div class="seating-plan-seat-banner">');
1796 seatBanner.html('<strong>' + (ret.seat_label_text || _x('Seat', 'label', 'event-tickets-with-ticket-scanner')) + ':</strong> ' +
1797 ret.seat_label + (ret.seat_category ? ' (' + ret.seat_category + ')' : ''));
1798 content.append(seatBanner);
1799
1800 // Plan description if available
1801 if (ret.seating_plan_description) {
1802 let descDiv = $('<div class="seating-plan-description">').text(ret.seating_plan_description);
1803 content.append(descDiv);
1804 }
1805
1806 // Venue image
1807 let imgContainer = $('<div class="seating-plan-image-container">');
1808 let img = $('<img>').attr('src', ret.seating_plan_image_url).attr('alt', ret.seating_plan_name || 'Venue');
1809 imgContainer.append(img);
1810 content.append(imgContainer);
1811
1812 modal.append(content);
1813
1814 // Footer with close button
1815 let footer = $('<div class="seating-plan-modal-footer">');
1816 $('<button class="button-ticket-options">').text(_x('Close', 'label', 'event-tickets-with-ticket-scanner')).on('click', function() {
1817 overlay.remove();
1818 }).appendTo(footer);
1819 modal.append(footer);
1820
1821 overlay.append(modal);
1822 $('body').append(overlay);
1823 }
1824
1825 // Load seating plan data via REST endpoint and show modal (lazy loading)
1826 function loadAndShowSeatingPlan(planId, seatId, ticketRet) {
1827 // Show loading overlay
1828 let loadingOverlay = $('<div class="seating-plan-modal-overlay">');
1829 let loadingModal = $('<div class="seating-plan-modal" style="text-align:center;padding:40px;">');
1830 loadingModal.html('<p>' + __('Loading seating plan...', 'event-tickets-with-ticket-scanner') + '</p>');
1831 loadingOverlay.append(loadingModal);
1832 $('body').append(loadingOverlay);
1833
1834 // Fetch seating plan data via REST endpoint
1835 _makeGet('seating_plan', {plan_id: planId, seat_id: seatId}, function(data) {
1836 loadingOverlay.remove();
1837 if (data) {
1838 showSeatingPlanModal(data, ticketRet);
1839 } else {
1840 alert(_x('Failed to load seating plan', 'label', 'event-tickets-with-ticket-scanner'));
1841 }
1842 }, function(response) {
1843 loadingOverlay.remove();
1844 alert(_x('Error loading seating plan', 'label', 'event-tickets-with-ticket-scanner'));
1845 });
1846 }
1847
1848 function showSeatingPlanModal(planData, ticketRet) {
1849 if (!planData) {
1850 alert(_x('Seating plan data not available', 'label', 'event-tickets-with-ticket-scanner'));
1851 return;
1852 }
1853
1854 // Create modal overlay
1855 let overlay = $('<div class="seating-plan-modal-overlay">').on('click', function(e) {
1856 if (e.target === this) {
1857 $(this).remove();
1858 }
1859 });
1860 let modal = $('<div class="seating-plan-modal seating-plan-modal-large">');
1861
1862 // Header with close button
1863 let header = $('<div class="seating-plan-modal-header">');
1864 $('<h3>').text(planData.planName || _x('Seating Plan', 'label', 'event-tickets-with-ticket-scanner')).appendTo(header);
1865 $('<button class="seating-plan-modal-close">&times;</button>').on('click', function() {
1866 overlay.remove();
1867 }).appendTo(header);
1868 modal.append(header);
1869
1870 // Content area with seating plan
1871 let content = $('<div class="seating-plan-modal-content">');
1872
1873 // Seat info banner
1874 let seatBanner = $('<div class="seating-plan-seat-banner">');
1875 seatBanner.html('<strong>' + (ticketRet.seat_label_text || _x('Seat', 'label', 'event-tickets-with-ticket-scanner')) + ':</strong> ' +
1876 ticketRet.seat_label + (ticketRet.seat_category ? ' (' + ticketRet.seat_category + ')' : ''));
1877 content.append(seatBanner);
1878
1879 // Plan description if available
1880 if (ticketRet.seating_plan_description) {
1881 let descDiv = $('<div class="seating-plan-description">').text(ticketRet.seating_plan_description);
1882 content.append(descDiv);
1883 }
1884
1885 // Build SVG seating plan using the same approach as seating_frontend.js
1886 let canvasContainer = $('<div class="seating-plan-canvas-container">');
1887 if (planData.seats && planData.seats.length > 0) {
1888 // Build full SVG with decorations, lines, labels, and seats
1889 let svgHtml = buildSeatingPlanSvg(planData, planData.currentSeatId);
1890 canvasContainer.html(svgHtml);
1891 } else if (planData.planImage) {
1892 // Fallback: show venue image only
1893 let imgContainer = $('<div class="seating-plan-image-container">');
1894 let img = $('<img>').attr('src', planData.planImage).attr('alt', planData.planName || 'Venue');
1895 imgContainer.append(img);
1896 canvasContainer.append(imgContainer);
1897 } else {
1898 // No visual data available
1899 canvasContainer.html('<p style="text-align:center;padding:40px;">' +
1900 _x('No seating plan visualization available.', 'label', 'event-tickets-with-ticket-scanner') + '</p>');
1901 }
1902
1903 content.append(canvasContainer);
1904 modal.append(content);
1905
1906 // Footer with close button
1907 let footer = $('<div class="seating-plan-modal-footer">');
1908 $('<button class="button-ticket-options">').text(_x('Close', 'label', 'event-tickets-with-ticket-scanner')).on('click', function() {
1909 overlay.remove();
1910 }).appendTo(footer);
1911 modal.append(footer);
1912
1913 overlay.append(modal);
1914 $('body').append(overlay);
1915 }
1916 function displayRedeemedTicketsInfo(data) {
1917 let div = $('<div>');
1918 let show = false;
1919 if (data._ret.tickets_redeemed_show) {
1920 show = true;
1921 $('<div style="color:black !important">').html(sprintf(/* translators: %d: amount redeemed tickets */__('%d tickets of this event (product) redeemed already by stats', 'event-tickets-with-ticket-scanner'), data._ret.tickets_redeemed)).appendTo(div);
1922 }
1923 if (data._ret.tickets_redeemed_show_c) {
1924 show = true;
1925 $('<div style="color:black !important">').html(sprintf(/* translators: %d: amount redeemed tickets */__('%d tickets of this event (product) redeemed already', 'event-tickets-with-ticket-scanner'), data._ret.tickets_redeemed_by_codes)).appendTo(div);
1926 }
1927 if (data._ret.tickets_redeemed_show_cn) {
1928 show = true;
1929 $('<div style="color:black !important">').html(sprintf(/* translators: %d: amount redeemed tickets */__('%d tickets of this event (product) not redeemed yet', 'event-tickets-with-ticket-scanner'), data._ret.tickets_redeemed_not_by_codes)).appendTo(div);
1930 }
1931 if (show) {
1932 div.css('text-align', 'center');
1933 div.css("padding-top", "10px");
1934 return div;
1935 }
1936 return '';
1937 }
1938 function updateTicketScannerInfoArea(content) {
1939 $('#ticket_scanner_info_area').html(content);
1940 if (toBool(myAjax.ticketScannerDisplayTimes)) {
1941 let data = system.data;
1942 if (data != null && typeof data != "undefined" && typeof data._ret != "undefined" && typeof data._ret._server != "undefined") {
1943 let div = $('<div style="padding-top:30px;">');
1944 div.append("Server: "+data._ret._server.time+" "+data._ret._server.timezone.timezone+" Offset: "+data._ret._server.timezone.timezone+"<br>");
1945 let date = new Date();
1946 div.append("Local: "+date+"<br>");
1947 system.TIMEAREA.html(div);
1948 }
1949 }
1950 }
1951 function updateLastScanTime() {
1952 let now = new Date();
1953 let timeStr = now.toLocaleTimeString();
1954 let el = $('#last_scan_time');
1955 if (el.length === 0) {
1956 system.TIMEAREA.prepend($('<div id="last_scan_time" style="font-size:0.85em;color:#666;padding:2px 0;">').html(
1957 sprintf(/* translators: %s: time of last scan */__('Last scan: %s', 'event-tickets-with-ticket-scanner'), timeStr)
1958 ));
1959 } else {
1960 el.html(sprintf(/* translators: %s: time of last scan */__('Last scan: %s', 'event-tickets-with-ticket-scanner'), timeStr));
1961 }
1962 }
1963 function displayTimezoneInformation(data) {
1964 let div = $('<div>').css('text-align', 'center');
1965 if (typeof system.PARA.displaytime !== "undefined") {
1966 div.css("padding", "10px;");
1967 //console.log(data);
1968 div.append("Ticket start timestamp: "+(data._ret.ticket_start_date_timestamp*1000)+"<br>");
1969 let d_t_s = new Date(data._ret.ticket_start_date_timestamp*1000);
1970 div.append("Ticket start timestamp date: "+d_t_s+"<br>");
1971
1972 div.append("Ticket end timestamp: "+(data._ret.ticket_end_date_timestamp*1000)+"<br>");
1973 let d_t_e = new Date(data._ret.ticket_end_date_timestamp*1000);
1974 div.append("Ticket end timestamp date: "+d_t_e+"<br>");
1975
1976 if (typeof data._ret._server !== "undefined") {
1977 try {
1978 div.append("Server timezone: "+data._ret._server.timezone.timezone+" Offset: "+data._ret._server.timezone.timezone+"<br>");
1979 div.append("Server time: "+data._ret._server.time+"<br>");
1980 div.append("UTC time: "+data._ret._server.UTC_time+"<br>");
1981 if (typeof data._ret.is_date_set != "undefined" && data._ret.is_date_set) {
1982 let date = new Date(data._ret.redeem_allowed_from);
1983 div.append('Redeem allowed from: '+date+'<br>');
1984 date = new Date(data._ret.redeem_allowed_until);
1985 div.append("Redeem allowed until: "+date+"<br>");
1986 }
1987 } catch(e) {
1988 //console.log(e);
1989 }
1990 }
1991
1992 let d_ts_n = new Date();
1993 div.append("Ticket scanner browser now-date: "+d_ts_n+"<br>");
1994 }
1995 return div;
1996 }
1997 function cleanPublicTicketNumber(code) {
1998 if (code) {
1999 return code.replace(/'/g, "-").trim();
2000 }
2001 return '';
2002 }
2003 function addInputField() {
2004 let div = $('<div>').css('text-align', 'center');
2005 $('<label for="barcode_scanner_input" class="form-label" style="color:#837878">').html(__('For QR code barcode scanner', 'event-tickets-with-ticket-scanner')).appendTo(div);
2006 $('<br>').appendTo(div);
2007 let inputField = $('<input style="width:70%;" name="barcode_scanner_input" placeholder="'+_x('Type in the ticket number and hit ENTER (optional to scanning)', 'attr', 'event-tickets-with-ticket-scanner')+'" type="text">')
2008 .appendTo(div)
2009 .on("change", ()=>{
2010 let code = cleanPublicTicketNumber(inputField.val());
2011 if (code != "") {
2012 clearOrderInfos();
2013 retrieveTicket(code, false, ()=>{
2014 inputField.focus();
2015 inputField.select();
2016 });
2017 }
2018 })
2019 .on("keypress", event=>{
2020 if (event.key === "Enter") {
2021 let code = cleanPublicTicketNumber(inputField.val());
2022 if (code != "") {
2023 event.preventDefault();
2024 clearOrderInfos();
2025 retrieveTicket(code, false, ()=>{
2026 inputField.focus();
2027 inputField.select();
2028 });
2029 }
2030 }
2031 });
2032 system.INPUTFIELD = div;
2033 }
2034 function addRemoveAuthTokenButton() {
2035 let div = $('<div style="padding-top:10px;">').css('text-align', 'center');
2036 $('<button style="background-color:red;color:white;">')
2037 .html("Remove Auth Token")
2038 .appendTo(div)
2039 .on("click", e=>{
2040 if (confirm("Do you want to delete the auth token?")) {
2041 setAuthToken();
2042 showScanOptions();
2043 }
2044 });
2045 system.AUTHTOKENREMOVEBUTTON = div;
2046 }
2047 function addClearCamDeviceButton() {
2048 let btn = $('<button>').html("Clear the stored cam device").on("click", event=>{
2049 _storeValue("ticketScannerCameraId", "", 1);
2050 window.location.reload(true);
2051 });
2052 if (!system.ADDITIONBUTTONS) system.ADDITIONBUTTONS = $('<div style="text-align:center;margin-top:20px;">');
2053 system.ADDITIONBUTTONS.append(btn);
2054 }
2055 function initStyle() {
2056 document.getElementsByClassName('ticket_content')[0].style.borderRadius="12px";
2057 let content = '';
2058 content += 'button.button-ticket-options {width:90%;margin-left:auto;margin-right:auto;margin-bottom:15px;display:block;border-radius:12px;padding:10px 15px;text-align:center;}';
2059 var tc = myAjax.ticketScannerThemeColor || '#008CBA';
2060 content += 'button.button-primary {background-color:'+tc+';color:white;border-color:'+tc+';}';
2061 content += '@media screen and (min-width: 720px) { button.button-ticket-options{width:50%;} }';
2062 // Seating Plan Modal Styles
2063 content += '.seating-plan-modal-overlay {position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;padding:10px;box-sizing:border-box;}';
2064 content += '.seating-plan-modal {background:#fff;border-radius:12px;max-width:95vw;max-height:95vh;width:800px;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,0.3);}';
2065 content += '.seating-plan-modal-large {width:90vw;max-width:1200px;}';
2066 content += '.seating-plan-modal-header {display:flex;justify-content:space-between;align-items:center;padding:15px 20px;border-bottom:1px solid #eee;background:#f5f5f5;}';
2067 content += '.seating-plan-modal-header h3 {margin:0;font-size:1.2em;color:#333;}';
2068 content += '.seating-plan-modal-close {background:none;border:none;font-size:28px;cursor:pointer;color:#666;padding:0 5px;line-height:1;}';
2069 content += '.seating-plan-modal-close:hover {color:#000;}';
2070 content += '.seating-plan-modal-content {flex:1;overflow:auto;padding:15px;}';
2071 content += '.seating-plan-seat-banner {background:#4CAF50;color:#fff;padding:12px 15px;border-radius:8px;margin-bottom:15px;text-align:center;font-size:1.1em;}';
2072 content += '.seating-plan-canvas-container {position:relative;background:#f9f9f9;border-radius:8px;overflow:hidden;min-height:200px;}';
2073 content += '.saso-seat-map-readonly {width:100%;height:auto;display:block;}';
2074 content += '.saso-seat-map-readonly .current-seat {animation:pulse-seat 1.5s ease-in-out infinite;}';
2075 content += '@keyframes pulse-seat {0%,100%{stroke-width:4px;} 50%{stroke-width:8px;}}';
2076 content += '.seating-plan-image-container {position:relative;width:100%;}';
2077 content += '.seating-plan-image-container img {width:100%;height:auto;display:block;}';
2078 content += '.seating-plan-seat-marker {position:absolute;transform:translate(-50%,-50%);width:40px;height:40px;background:#4CAF50;border:3px solid #ff0000;border-radius:50%;display:flex;align-items:center;justify-content:center;animation:pulse-marker 1.5s ease-in-out infinite;box-shadow:0 2px 10px rgba(0,0,0,0.3);}';
2079 content += '.seating-plan-seat-marker span {color:#fff;font-weight:bold;font-size:10px;text-align:center;}';
2080 content += '@keyframes pulse-marker {0%,100%{transform:translate(-50%,-50%) scale(1);} 50%{transform:translate(-50%,-50%) scale(1.2);}}';
2081 content += '.seating-plan-modal-footer {padding:15px 20px;border-top:1px solid #eee;text-align:center;}';
2082 content += '.seating-plan-modal-footer button {width:auto;display:inline-block;padding:10px 30px;}';
2083 content += '.btn-seating-plan {background:#2196F3 !important;color:#fff !important;}';
2084 content += '.btn-venue-image {background:#FF9800 !important;color:#fff !important;}';
2085 content += '.seating-plan-description {background:#f0f0f0;padding:10px 15px;border-radius:6px;margin-bottom:15px;color:#555;font-size:0.95em;}';
2086 addStyleCode(content);
2087 }
2088 function refreshNoncePeriodically() {
2089 // check if the last check of nonce is older than 4 minutes
2090 // do a ping to get the new nonce
2091 setInterval(()=>{
2092 let last_check = system.last_nonce_check;
2093 if (last_check == null || last_check == "") {
2094 last_check = 0;
2095 }
2096 let now = new Date().getTime();
2097 if (now - last_check > 240000) {
2098 _makeGet('ping', [], data=>{
2099 });
2100 }
2101 }, 60000);
2102 }
2103
2104 function speak(text) {
2105 if (TTS != null) {
2106 TTS.speak(text);
2107 }
2108 }
2109 /**
2110 * tts.js — Robust Web TTS for Ticket Scanner
2111 * - No UI. No static strings.
2112 * - Optional language override per call; null/undefined → browser language with sensible fallbacks.
2113 * - Safe across Chrome/Edge/Safari/Firefox (Web Speech API).
2114 * - Handles multiple calls, cancels overlaps, async voice loading, and user-activation policy.
2115 */
2116 function initTTS() {
2117 const RESULT = Object.freeze({
2118 OK: "ok",
2119 UNSUPPORTED: "unsupported",
2120 NEEDS_ACTIVATION: "needs_activation",
2121 BUSY: "busy",
2122 ERROR: "error",
2123 });
2124
2125 let voices = [];
2126 let voicesReady = false;
2127 let speaking = false;
2128 let activated = false;
2129
2130 // ---------- feature & policy helpers ----------
2131 const hasTTS = () =>
2132 typeof window !== "undefined" &&
2133 "speechSynthesis" in window &&
2134 "SpeechSynthesisUtterance" in window;
2135
2136 const isUserActivated = () => {
2137 const ua = navigator.userActivation;
2138 // Some browsers expose hasBeenActive, others just isActive; either is fine to proceed after a user gesture.
2139 return !!(ua && (ua.isActive || ua.hasBeenActive));
2140 };
2141
2142 const pageOK = () =>
2143 (typeof document === "undefined" || document.visibilityState === "visible") &&
2144 (typeof document === "undefined" || !document.hasFocus || document.hasFocus());
2145
2146 // ---------- language & voices ----------
2147 function detectLang() {
2148 const prefs = Array.isArray(navigator.languages) && navigator.languages.length
2149 ? navigator.languages
2150 : [navigator.language || "en-US"];
2151
2152 const normalized = prefs
2153 .filter(Boolean)
2154 .map(l => l.replace("_", "-"))
2155 .map(l => (l.length === 2
2156 ? (l === "de" ? "de-DE" : l === "en" ? "en-US" : l)
2157 : l));
2158
2159 const fallbacks = ["en-US", "en-GB", "de-DE", "fr-FR", "es-ES", "it-IT"];
2160 return [...normalized, ...fallbacks].find(Boolean);
2161 }
2162
2163 function loadVoices() {
2164 try {
2165 voices = window.speechSynthesis.getVoices() || [];
2166 if (!voices.length) {
2167 window.speechSynthesis.onvoiceschanged = () => {
2168 voices = window.speechSynthesis.getVoices() || [];
2169 voicesReady = true;
2170 };
2171 } else {
2172 voicesReady = true;
2173 }
2174 } catch { /* noop */ }
2175 }
2176
2177 function pickVoice(langCode) {
2178 if (!voices || !voices.length) return null;
2179 // exact match > language prefix match
2180 return (
2181 voices.find(v => v.lang === langCode) ||
2182 voices.find(v => v.lang && v.lang.toLowerCase().startsWith((langCode || "").toLowerCase().slice(0, 2))) ||
2183 null
2184 );
2185 }
2186
2187 // ---------- activation ----------
2188 /**
2189 * Prime TTS after a user gesture (click/tap). Silent, fast, idempotent.
2190 * Call this once from your own UI handler (e.g., "Start scanning").
2191 */
2192 function prime(lang) {
2193 if (!hasTTS()) return RESULT.UNSUPPORTED;
2194 loadVoices();
2195 const chosen = lang ?? detectLang();
2196 try {
2197 const u = new SpeechSynthesisUtterance(".");
2198 u.lang = chosen;
2199 u.volume = 0; // silent
2200 u.rate = 1; u.pitch = 1;
2201 const v = pickVoice(chosen);
2202 if (v) u.voice = v;
2203 window.speechSynthesis.cancel();
2204 window.speechSynthesis.speak(u);
2205 activated = true;
2206 return RESULT.OK;
2207 } catch {
2208 return RESULT.ERROR;
2209 }
2210 }
2211
2212 // ---------- speak ----------
2213 /**
2214 * Speak a text. Returns a Promise<RESULT>.
2215 * @param {string} text
2216 * @param {{ lang?: string|null, rate?: number, pitch?: number }} [opts]
2217 */
2218 function speak(text, opts = {}) {
2219 return new Promise((resolve) => {
2220 if (!text || typeof text !== "string") return resolve(RESULT.ERROR);
2221 if (!hasTTS()) return resolve(RESULT.UNSUPPORTED);
2222 if (!pageOK()) return resolve(RESULT.NEEDS_ACTIVATION);
2223
2224 // If site hasn’t called prime() under a user gesture, some browsers will block.
2225 // We surface that cleanly so the host app can call TTS.prime() from a click/tap.
2226 if (!activated && !isUserActivated()) return resolve(RESULT.NEEDS_ACTIVATION);
2227
2228 try {
2229 if (speaking) {
2230 // cancel current queue/utterance to avoid overlaps
2231 window.speechSynthesis.cancel();
2232 speaking = false;
2233 }
2234
2235 if (!voicesReady) loadVoices();
2236
2237 const lang = (opts.lang === null || typeof opts.lang === "undefined")
2238 ? detectLang()
2239 : (opts.lang || detectLang());
2240
2241 const u = new SpeechSynthesisUtterance(text);
2242 u.lang = lang;
2243 u.rate = (typeof opts.rate === "number" && opts.rate > 0) ? opts.rate : 1;
2244 u.pitch = (typeof opts.pitch === "number" && opts.pitch > 0) ? opts.pitch : 1;
2245
2246 const v = pickVoice(lang);
2247 if (v) u.voice = v;
2248
2249 u.onstart = () => { speaking = true; };
2250 u.onend = () => { speaking = false; resolve(RESULT.OK); };
2251 u.onerror = () => { speaking = false; resolve(RESULT.ERROR); };
2252
2253 window.speechSynthesis.cancel(); // clear queue
2254 window.speechSynthesis.speak(u);
2255 } catch {
2256 resolve(RESULT.ERROR);
2257 }
2258 });
2259 }
2260
2261 return { prime, speak, RESULT };
2262 }
2263
2264 function starten() {
2265 $ = jQuery;
2266 initStyle();
2267 addMetaTag("viewport", "width=device-width, initial-scale=1");
2268 $('#reader').html(_getSpinnerHTML());
2269 _makeGet('ping', [], data=>{
2270 system.data = data; // initialer daten empfang mit options
2271 system.img_pfad = data.img_pfad;
2272 system.PARA = basics_ermittelURLParameter();
2273
2274 if (toBool(myAjax.ticketScannerDontShowOptionControls)) {
2275 setRedeemImmediately(toBool(myAjax.ticketScannerScanAndRedeemImmediately));
2276 setDistractFree(toBool(myAjax.ticketScannerHideTicketInformation));
2277 setStartCamWithoutButtonClicked(toBool(myAjax.ticketScannerStartCamWithoutButtonClicked));
2278 setVibrate(toBool(myAjax.ticketScannerVibrate));
2279 } else {
2280 if (system.PARA.redeemauto || _loadValue("ticket_scanner_operating_option.redeem_auto") == "1" || setRedeemImmediately(toBool(myAjax.ticketScannerScanAndRedeemImmediately))) {
2281 setRedeemImmediately(true);
2282 }
2283 if (system.PARA.distractfree || _loadValue("ticket_scanner_operating_option.distract_free") == "1" || toBool(myAjax.ticketScannerHideTicketInformation)) {
2284 setDistractFree(true);
2285 }
2286 if (system.PARA.distractfreeshowshortdesc || _loadValue("ticket_scanner_operating_option.distract_free_show_short_desc") == "1" || toBool(myAjax.ticketScannerHideTicketInformationShowShortDesc)) {
2287 setDistractFreeShowShortDesc(true);
2288 }
2289 if (system.PARA.startcam || _loadValue("ticket_scanner_operating_option.ticketScannerStartCamWithoutButtonClicked") == "1" || toBool(myAjax.ticketScannerStartCamWithoutButtonClicked)) {
2290 setStartCamWithoutButtonClicked(true);
2291 }
2292 if (system.PARA.speak || _loadValue("ticket_scanner_operating_option.speak") == "1" || toBool(myAjax.ticketScannerSpeakText)) {
2293 setSpeakCheckbox(true);
2294 }
2295 if (_loadValue("ticket_scanner_operating_option.vibrate") == "1" || toBool(myAjax.ticketScannerVibrate)) {
2296 setVibrate(true);
2297 }
2298 }
2299
2300 initAuthToken();
2301 addInputField();
2302 addClearCamDeviceButton();
2303 addRemoveAuthTokenButton();
2304 showScanOptions();
2305 refreshNoncePeriodically();
2306
2307 if (system.PARA.code) {
2308 system.code = system.PARA.code;
2309 if (system.code != "") {
2310 system.code = cleanPublicTicketNumber(system.code);
2311 system.INPUTFIELD.val(system.code);
2312 retrieveTicket(system.code);
2313 }
2314 } else {
2315 startScanner();
2316 //showScanNextTicketButton();
2317 }
2318
2319 registerServiceWorker();
2320 });
2321
2322 }
2323
2324 function speakText(text, lang) {
2325 //console.log(text);
2326 if (ticket_scanner_operating_option.speak) {
2327 try {
2328 if (!('speechSynthesis' in window)) return; // kein TTS support
2329
2330 // Browser-Sprache als Fallback
2331 const language = lang || navigator.language || "en-US";
2332
2333 // Cancel laufende Ausgabe
2334 window.speechSynthesis.cancel();
2335
2336 // Neues Utterance erzeugen
2337 const utter = new SpeechSynthesisUtterance(text);
2338 utter.lang = language;
2339 utter.rate = 1;
2340 utter.pitch = 1;
2341
2342 // Fehlerbehandlung
2343 utter.onerror = (e) => console.warn("TTS error:", e);
2344
2345 // Aussprechen
2346 window.speechSynthesis.speak(utter);
2347 } catch (e) {
2348 console.error("TTS failed:", e);
2349 }
2350 }
2351 }
2352
2353 function registerServiceWorker() {
2354 if ('serviceWorker' in navigator && myAjax._pwaSWUrl) {
2355 var scannerScope = window.location.pathname;
2356 if (scannerScope.charAt(scannerScope.length - 1) !== '/') scannerScope += '/';
2357
2358 navigator.serviceWorker.register(myAjax._pwaSWUrl, { scope: scannerScope })
2359 .then(function(registration) {
2360 setInterval(function() { registration.update(); }, 3600000);
2361 })
2362 .catch(function() {
2363 // Not critical - scanner works without SW
2364 });
2365 }
2366 }
2367
2368 var $;
2369 //window.onload = starten;
2370 starten();
2371 } );