chore(eslint): ESLint setup (#6708)
[openemr.git] / portal / sign / assets / signer_api.js
blob138eb5405ac296c1336175a9cd00d76d839d5ba4
1 /**
2  * @package   OpenEMR
3  * @link      http://www.open-emr.org
4  * @author    Jerry Padgett <sjpadgett@gmail.com>
5  * @copyright Copyright (c) 2016-2022 Jerry Padgett <sjpadgett@gmail.com>
6  * @license   https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
7  */
9 const signhere = "data:image/svg+xml,%3C%3Fxml version='1.0' standalone='no'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 20010904//EN' 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd'%3E%3Csvg version='1.0' xmlns='http://www.w3.org/2000/svg' width='852.000000pt' height='265.000000pt' viewBox='0 0 852.000000 265.000000' preserveAspectRatio='xMidYMid meet'%3E%3Cg transform='translate(0.000000,265.000000) scale(0.100000,-0.100000)'%0Afill='%23000000' stroke='none'%3E%3Cpath d='M390 1534 c-19 -14 -67 -57 -107 -96 l-71 -71 -31 34 c-16 19 -38 49%0A-48 67 -18 32 -53 44 -53 18 0 -17 47 -111 72 -144 17 -23 17 -25 -21 -75 -46%0A-62 -85 -143 -77 -163 3 -8 14 -14 25 -14 15 0 27 15 45 53 23 52 71 117 85%0A117 4 0 15 -15 25 -32 11 -18 34 -50 52 -70 18 -20 40 -51 49 -67 9 -19 23%0A-31 35 -31 38 0 18 71 -34 119 -33 31 -89 117 -84 130 1 6 49 57 106 115 56%0A57 102 111 102 120 0 23 -29 18 -70 -10z'/%3E%3C/g%3E%3C/svg%3E";
11 let adminName = '';
12 let $lastEl = '';
13 let isAdmin = false;
14 let type = '';
15 if (typeof isPortal === 'undefined') {
16     var isPortal = 0;
19 if (typeof ptName === 'undefined') {
20     var ptName = '';
22 if (typeof webRoot === 'undefined' && typeof top.webroot_url !== 'undefined') {
23     var webRoot = top.webroot_url;
25 if (typeof isModule === 'undefined') {
26     var isModule = false;
29 if (typeof cpid === 'undefined') {
30     var cpid;
33 if (typeof cuser === 'undefined') {
34     var cuser;
37 function signerAlertMsg(message, timer = 5000, type = 'danger', size = '') {
38     $('#signerAlertBox').remove();
39     size = (size == 'lg') ? 'left:25%;width:50%;' : 'left:35%;width:30%;';
40     let style = "position:fixed;top:25%;" + size + " bottom:0;z-index:1020;z-index:5000";
41     $("body").prepend("<div class='container text-center' id='signerAlertBox' style='" + style + "'></div>");
42     let mHtml = '<div id="alertMessage" class="alert alert-' + type + ' alert-dismissable">' +
43         '<button type="button" class="close btn btn-link btn-cancel" data-dismiss="alert" aria-hidden="true">&times;</button>' +
44         '<h5 class="alert-heading text-center">Alert!</h5><hr>' +
45         '<p>' + message + '</p>' +
46         '</div>';
47     $('#signerAlertBox').append(mHtml);
48     $('#alertMessage').on('closed.bs.alert', function () {
49         clearTimeout(AlertMsg);
50         $('#signerAlertBox').remove();
51     });
52     let AlertMsg = setTimeout(function () {
53         $('#alertMessage').fadeOut(800, function () {
54             $('#signerAlertBox').remove();
55         });
56     }, timer);
59 function getSignature(othis, isInit = false, returnSignature = false) {
60     return new Promise(resolve => {
61             let signer, signerType = "";
62             let libUrl = "./";
64             if ($(othis).attr('src') != signhere && !isInit) {
65                 $(othis).attr('src', signhere);
66                 return;
67             }
68             if (typeof webRoot !== 'undefined' && webRoot !== null) {
69                 libUrl = webRoot + '/portal/';
70             } else {
71                 libUrl = top.webroot_url ? (top.webroot_url + '/portal/') : "./";
72             }
74             if (typeof cpid === 'undefined' && typeof cuser === 'undefined') {
75                 cpid = $(othis).data('pid');
76                 cuser = $(othis).data('user');
77             }
78             let otype = $(othis).attr('data-type');
79             type = otype;
80             if (typeof otype === 'undefined' || otype === null) {
81                 otype = $(othis).data('type');
82             }
83             if (otype == 'admin-signature') {
84                 signer = adminName ? adminName : cuser;
85                 signerType = "admin-signature";
86                 $("#isAdmin").prop('checked', true);
87                 isAdmin = true;
88             } else if (otype == 'witness-signature') {
89                 signer = 'Witness';
90                 signerType = "witness-signature";
91                 $("#isAdmin").prop('checked', false);
92                 isAdmin = false;
93                 return false;
94             } else {
95                 signer = ptName;
96                 signerType = "patient-signature";
97                 $("#isAdmin").prop('checked', false);
98                 isAdmin = false;
99             }
101             let params = {
102                 pid: cpid,
103                 user: cuser,
104                 is_portal: isPortal,
105                 type: signerType
106             };
108             let url = libUrl + "sign/lib/show-signature.php";
109             fetch(url, {
110                 credentials: 'include',
111                 method: 'POST',
112                 body: JSON.stringify(params),
113                 headers: {
114                     'Accept': 'application/json, text/plain, */*',
115                     'Content-Type': 'application/json'
116                 }
117             }).then(signature => signature.json()).then(signature => {
118                 if (returnSignature === true) {
119                     return signature;
120                 }
121                 placeSignature(signature, othis).then(function (r) {
122                     resolve(r)
123                 })
125             }).catch(error => signerAlertMsg(error));
126         }
127     )
130 function placeSignature(responseData, el) {
131     return new Promise(resolve => {
132         if (responseData.message === "error") {
133             $(el).attr('src', "");
134             $(el).attr('alt', "No Signature on File");
135             signerAlertMsg('Error Patient and or User Id missing');
136             return;
137         } else if (responseData.message === "insert error") {
138             $(el).attr('src', "");
139             $(el).attr('alt', "No Signature on File");
140             signerAlertMsg('Error adding signature');
141             return;
142         } else if (responseData.message === "waiting" && $(el).attr('data-type') === 'patient-signature') {
143             $(el).attr('src', "");
144             $(el).attr('alt', "No Signature on File");
145             $("#isAdmin").attr('checked', false);
146             return;
147         } else if (responseData.message === "waiting" && $(el).attr('data-type') === 'admin-signature') {
148             $(el).attr('src', "");
149             $(el).attr('alt', "No Signature on File");
150             $("#isAdmin").attr('checked', true);
151             return;
152         } else if (responseData.message === "waiting") {
153             $(el).attr('src', "");
154             $(el).attr('alt', "No Signature on File");
155             return;
156         }
157         let i = new Image();
158         i.onload = function () {
159             $(el).attr('src', i.src)
160             resolve('done'); // display image
161         };
162         if (!isDataURL(responseData)) {
163             alert("Invalid Signature.");
164             resolve('Error');
165             return false;
166         }
167         i.src = responseData; // load image
168     })
171 function archiveSignature(signImage = '', edata = '') {
172     let libUrl, signer, signerType = "";
173     let pid = 0;
174     let data = {};
176     if (typeof webRoot !== 'undefined' && webRoot !== null) {
177         libUrl = webRoot + '/portal/';
178     } else {
179         libUrl = "./";
180     }
182     if (edata) {
183         data = {
184             pid: edata.data.cpid,
185             user: edata.data.cuser,
186             is_portal: isPortal,
187             signer: edata.data.signer,
188             type: edata.data.type,
189             output: signImage
190         };
191     } else {
192         if ($("#isAdmin").is(':checked') === false) {
193             pid = cpid;
194             signer = ptName ? ptName : cuser;
195             signerType = "patient-signature";
196         } else {
197             pid = 0;
198             signer = adminName ? adminName : cuser;
199             signerType = "admin-signature";
200         }
201         data = {
202             pid: pid,
203             user: cuser,
204             is_portal: isPortal,
205             signer: signer,
206             type: signerType,
207             output: signImage
208         };
209     }
210     let url = libUrl + "sign/lib/save-signature.php";
211     fetch(url, {
212         credentials: 'include',
213         method: 'POST',
214         body: JSON.stringify(data),
215         headers: {
216             'Content-Type': 'application/json',
217             'Connection': 'close'
218         }
219     }).then(response => response.json()).then(function (response) {
220             $("#openSignModal").modal("hide");
221         }
222     ).catch(error => signerAlertMsg(error));
224     return true;
227 function isDataURL(dataUrl = '') {
228     return !!dataUrl.match(isDataURL.regex);
231 isDataURL.regex = /^\s*data:([a-z]+\/[a-z]+(;[a-z\-]+=[a-z\-]+)?)?(;base64)?,[a-z0-9!$&',()*+;=\-._~:@\/?%\s]*\s*$/i;
233 // call if need to bind pen clicks after a dynamic template load. ie templates.
234 var bindFetch = '';
236 // fetch modal template and append to body.
238 $(function () {
239     let url = top.webroot_url ? top.webroot_url : webRoot;
240     url += "/portal/sign/assets/signer_modal.php?isPortal=" + encodeURIComponent(isPortal);
241     fetch(url, {
242         credentials: 'include'
243     }).then(jsonTemplate => jsonTemplate.json()).then(jsonTemplate => {
244         $("body").append(jsonTemplate);
245     }).then(function () {
246         initSignerApi();
247     }).catch((error) => alert("Modal Template Fetch:" + error));
250 function initSignerApi() {
251     // ya think there'd be more!
252     function callModal(e) {
253         cpid = e.data.cpid;
254         cuser = e.data.cuser;
255         let type = e.data.type;
256         let signerName = e.data.signerName || '';
258         $('#openSignModal #name').val(signerName);
259         $('#openSignModal #labelName').html("&nbsp;" + msgSignator + ":&nbsp;<b>" + signerName + "</b>");
260         $('#openSignModal #pid').val(cpid);
261         $('#openSignModal #user').val(cuser);
262         $('#openSignModal #signatureModal').data('type', type);
263         if (type === 'admin-signature' && isPortal) {
264             signerAlertMsg(xl('Signer Pad not available for this signature type!', 2000));
265             return false;
266         }
267         if (type === 'admin-signature') {
268             adminName = signerName;
269             $("#isAdmin").prop('checked', true);
270             isAdmin = true;
271         } else {
272             ptName = signerName;
273             $("#isAdmin").prop('checked', false);
274             isAdmin = false;
275         }
276         $("#openSignModal").modal('show');
277     }
279     function doConfirm(result) {
280         placeSignature(result.data.signature, $lastEl);
281     }
283     // global binding for signer form/template tag clicks.
284     // gets called after a new/edited template gets loaded.
285     // mostly used with Patient Documents list however,
286     // existing portal workflow is legacy and will deprecate here soon.
287     bindFetch = function () {
288         $("img[data-action=fetch_signature]").on("click", function (e) {
289             e.stopPropagation();
290             e.preventDefault();
291             $lastEl = $(this);
292             let pid = $lastEl.data('pid');
293             let user = $lastEl.data('user');
294             let type = $lastEl.data('type');
295             $('#openSignModal #signatureModal').data('type', type);
296             let signerName = '';
297             let url = webRoot + "/portal/sign/lib/show-signature.php";
298             if (!cpid) {
299                 cpid = pid;
300             }
301             if (!cuser) {
302                 cuser = user;
303             }
304             if (type === "admin-signature" && isPortal) {
305                 // don't allow patient to change user signature.
306                 return false;
307             } else if (isPortal && type !== "witness-signature") {
308                 getSignature(this);
309                 return false;
310             }
311             $lastEl.attr('src', signhere);
313             fetch(url, {
314                 credentials: 'include',
315                 method: 'POST',
316                 body: JSON.stringify({
317                     pid: pid,
318                     user: user,
319                     type: type,
320                     is_portal: isPortal,
321                     mode: 'fetch_info'
322                 }),
323                 headers: {
324                     'Accept': 'application/json, text/plain, */*',
325                     'Content-Type': 'application/json'
326                 }
327             }).then(response => response.json()).then(function (response) {
328                 let signerName = '';
329                 if (type === "admin-signature") {
330                     signerName = response.userName;
331                     adminName = signerName;
332                 } else {
333                     signerName = response.ptName;
334                     ptName = signerName;
335                 } // response.signature is available if needed in future.
336                 let e = [];
337                 e.data = {
338                     cpid: pid,
339                     cuser: user,
340                     type: type,
341                     signerName: signerName
342                 };
343                 callModal(e);
344             }).catch(error => signerAlertMsg(error));
345         });
347         $(function () {
348             // default all signatures to icon regardless of new or edit..
349             $(".signature").each(function (index, value) {
350                 if (!$(this).attr('src'))
351                     $(this).attr('src', signhere);
352             });
353         });
354     };
356     // initial binds for form pen clicks
357     bindFetch();
359 //-------------------- Continue loading seq with init of modal buttons and various bindings --------------------//
360     $(function (global) {
361         var wrapper = document.getElementById("openSignModal");
362         var canvasOptions = {
363             minWidth: 3.00,
364             maxWidth: 5.00,
365             penColor: 'rgb(0, 0, 255)',
366         };
367         var openPatientButton = document.querySelector("[data-type=patient-signature]");
368         var openAdminButton = document.querySelector("[data-type=admin-signature]");
369         var placeSignatureButton = wrapper.querySelector("[data-action=place]");
370         var showSignature = wrapper.querySelector("[data-action=show]");
371         var saveSignature = wrapper.querySelector("[data-action=save_signature]");
372         var clearButton = wrapper.querySelector("[data-action=clear]");
373         var canvas = wrapper.querySelector("canvas");
374         let signaturePad;
376         // this offsets signature image to center on element somewhat
377         // on any form (css) box height:70px length:auto center at 20px.
378         /*$(function (e) {
379             let els = this.querySelectorAll("img[data-action=fetch_signature]");
380             let i;
381             for (i = 0; i < els.length; i++) {
382                 els[i].style.top = (els[i].offsetTop - 20) + 'px';
383                 els[i].setAttribute("data-offset", true);
384             }
385         });*/
387         $("#openSignModal .close").on("click", function (e) {
388             signaturePad.clear();
389         });
391         if (typeof placeSignatureButton === 'undefined' || !placeSignatureButton) {
392             placeSignatureButton = wrapper.querySelector("[data-action=place]");
393         }
394         $("#openSignModal").on('show.bs.modal', function (e) {
395             let type = $('#openSignModal #signatureModal').data('type');
396             if (type === 'admin-signature' && isPortal) {
397                 signerAlertMsg('Signer Pad not available for this signature type!', 2000);
398                 return false;
399             }
400             if (type === 'witness-signature') {
401                 placeSignatureButton.classList.add('d-none');
402             } else {
403                 placeSignatureButton.classList.remove('d-none');
404             }
405         });
406         // for our dynamically added modal
407         $("#openSignModal").on('shown.bs.modal', function (e) {
408             let type = $('#openSignModal #signatureModal').data('type');
409             if (type) {
410                 if (type === "admin-signature") {
411                     $("#isAdmin").prop('checked', true);
412                     placeSignatureButton.setAttribute("data-type", type);
413                     isAdmin = true;
414                 } else {
415                     $("#isAdmin").prop('checked', false);
416                     placeSignatureButton.setAttribute("data-type", type);
417                     isAdmin = false;
418                 }
419                 $('#signatureModal').data('type', type);
420             }
421             let showElement = document.getElementById('signatureModal');
422             $('#signatureModal').attr('src', signhere);
423             $("#openSignModal").modal({backdrop: false});
424             $('html').css({
425                 'overflow': 'hidden'
426             });
427             $(this).css({
428                 'padding-right': '0px'
429             });
430             $('body').bind('selectstart', function () {
431                 return false;
432             });
433             $(this).modal('handleUpdate');
434         }).on('shown.bs.modal', function (e) { // yes two shown events
435             signaturePad = new SignaturePad(canvas, canvasOptions);
436             resizeCanvas();
437         }).on('hide.bs.modal', function () {
438             if ((typeof $lastEl !== 'undefined' || !$lastEl) && typeof event === "undefined") {
439                 if (!signaturePad.isEmpty()) {
440                     let dataURL = signaturePad.toDataURL();
441                     placeSignature(dataURL, $lastEl);
442                 }
443             }
444         }).on('hidden.bs.modal', function () {
445             $('html').css({
446                 'overflow': 'inherit'
447             });
448             $('body').unbind('selectstart');
449         });
451         clearButton.addEventListener("click", function (event) {
452             signaturePad.clear();
453         });
455         saveSignature.addEventListener("click", function (event) {
456             if (signaturePad.isEmpty()) {
457                 signerAlertMsg(msgNeedSign, 3000);
458                 return false;
459             }
460             let signerName, type = '';
461             type = $('#signatureModal').data('type');
462             if ($("#isAdmin").is(':checked') === true || type === 'admin-signature') {
463                 isAdmin = true;
464             }
465             if (type === 'witness-signature') {
466                 let dataURL = signaturePad.toDataURL();
467                 let e = [];
468                 e.data = {
469                     cpid: cpid,
470                     cuser: cuser,
471                     type: type,
472                     signer: 'Witness Signature'
473                 };
474                 archiveSignature(encodeURIComponent(dataURL), e);
475                 return false;
476             }
477             webRoot = webRoot ? webRoot : top.webroot_url;
478             let url = webRoot + "/portal/sign/lib/show-signature.php";
479             fetch(url, {
480                 credentials: 'include',
481                 method: 'POST',
482                 body: JSON.stringify({
483                     pid: cpid,
484                     user: cuser,
485                     type: type,
486                     is_portal: isPortal,
487                     mode: 'fetch_info'
488                 }),
489                 headers: {
490                     'Accept': 'application/json, text/plain, */*',
491                     'Content-Type': 'application/json'
492                 }
493             }).then(response => response.json()).then(function (response) {
494                 signerName = ptName = response.ptName;
495                 if (isAdmin) {
496                     signerName = adminName = response.userName;
497                 }
498                 let dataURL = signaturePad.toDataURL();
499                 let e = [];
500                 e.data = {
501                     cpid: cpid,
502                     cuser: cuser,
503                     type: type,
504                     signer: signerName
505                 };
506                 archiveSignature(encodeURIComponent(dataURL), e);
507             });
508         });
510         placeSignatureButton.addEventListener("click", function (event) {
511             let thisElement = $(this);
512             getSignature(thisElement, true).then(r => {
513                 let imgurl = thisElement.attr('src');
514                 signaturePad.fromDataURL(imgurl).then(r => {
516                 });
517             });
518         });
520         if (showSignature !== null)
521             showSignature.addEventListener("click", function (event) { // for modal view
522                 let showElement = document.getElementById('signatureModal');
523                 getSignature(showElement);
524             });
526         function resizeCanvas() {
527             let ratio = Math.max(window.devicePixelRatio || 1, 1);
528             canvas.width = canvas.offsetWidth * ratio;
529             canvas.height = canvas.offsetHeight * ratio;
530             canvas.getContext("2d").scale(ratio, ratio);
531         }
533         // this is nifty JS download. leaving for future.
534         // plus someone may come across and find useful.
535         function download(dataURL, filename) {
536             let blob = dataURLToBlob(dataURL);
537             let url = window.URL.createObjectURL(blob);
538             let a = document.createElement("a");
539             a.style = "display: none";
540             a.href = url;
541             a.download = filename;
542             document.body.appendChild(a);
543             a.click();
544             window.URL.revokeObjectURL(url);
545         }
547         function dataURLToBlob(dataURL) {
548             // Code taken from https://github.com/ebidel/filer.js
549             let parts = dataURL.split(';base64,');
550             let contentType = parts[0].split(":")[1];
551             let raw = window.atob(parts[1]);
552             let rawLength = raw.length;
553             let uInt8Array = new Uint8Array(rawLength);
555             for (var i = 0; i < rawLength; ++i) {
556                 uInt8Array[i] = raw.charCodeAt(i);
557             }
559             return new Blob([uInt8Array], {type: contentType});
560         }
562         // TODO create method to remove line on canvas save
563         function drawSignatureLine() {
564             let context = canvas.getContext('2d');
565             context.lineWidth = .4;
566             context.strokeStyle = '#333';
567             context.beginPath();
568             context.moveTo(0, 200);
569             context.lineTo(900, 200);
570             context.stroke();
571         }
573         // resize  event and initial resize
574         window.onresize = function () {
575             resizeCanvas();
576         };
577         resizeCanvas();
578     }); // dom