]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/circ/selfcheck/selfcheck.js
don't forget to fetch the org setting at load time
[Evergreen.git] / Open-ILS / web / js / ui / default / circ / selfcheck / selfcheck.js
1 dojo.require('dojo.date.locale');
2 dojo.require('dojo.date.stamp');
3 dojo.require('openils.CGI');
4 dojo.require('openils.Util');
5 dojo.require('openils.User');
6 dojo.require('openils.Event');
7 dojo.require('openils.widget.ProgressDialog');
8
9 dojo.requireLocalization('openils.circ', 'selfcheck');
10 var localeStrings = dojo.i18n.getLocalization('openils.circ', 'selfcheck');
11
12
13 const SET_BARCODE_REGEX = 'opac.barcode_regex';
14 const SET_PATRON_TIMEOUT = 'circ.selfcheck.patron_login_timeout';
15 const SET_AUTO_OVERRIDE_EVENTS = 'circ.selfcheck.auto_override_checkout_events';
16 const SET_PATRON_PASSWORD_REQUIRED = 'circ.selfcheck.patron_password_required';
17 const SET_AUTO_RENEW_INTERVAL = 'circ.checkout_auto_renew_age';
18 const SET_WORKSTATION_REQUIRED = 'circ.selfcheck.workstation_required';
19 const SET_ALERT_POPUP = 'circ.selfcheck.alert.popup';
20 const SET_ALERT_SOUND = 'circ.selfcheck.alert.sound';
21
22 function SelfCheckManager() {
23
24     this.cgi = new openils.CGI();
25     this.staff = null; 
26     this.workstation = null;
27     this.authtoken = null;
28
29     this.patron = null; 
30     this.patronBarcodeRegex = null;
31
32     // current item barcode
33     this.itemBarcode = null; 
34
35     // are we currently performing a renewal?
36     this.isRenewal = false; 
37
38     // dict of org unit settings for "here"
39     this.orgSettings = {};
40
41     // Construct a mock checkout for debugging purposes
42     if(this.mockCheckouts = this.cgi.param('mock-circ')) {
43
44         this.mockCheckout = {
45             payload : {
46                 record : new fieldmapper.mvr(),
47                 copy : new fieldmapper.acp(),
48                 circ : new fieldmapper.circ()
49             }
50         };
51
52         this.mockCheckout.payload.record.title('Jazz improvisation for guitar');
53         this.mockCheckout.payload.record.author('Wise, Les');
54         this.mockCheckout.payload.record.isbn('0634033565');
55         this.mockCheckout.payload.copy.barcode('123456789');
56         this.mockCheckout.payload.circ.renewal_remaining(1);
57         this.mockCheckout.payload.circ.parent_circ(1);
58         this.mockCheckout.payload.circ.due_date('2012-12-21');
59     }
60 }
61
62
63
64 /**
65  * Fetch the org-unit settings, initialize the display, etc.
66  */
67 SelfCheckManager.prototype.init = function() {
68
69     this.staff = openils.User.user;
70     this.workstation = openils.User.workstation;
71     this.authtoken = openils.User.authtoken;
72     this.loadOrgSettings();
73
74     // workstation is required but none provided
75     if(this.orgSettings[SET_WORKSTATION_REQUIRED] && !this.workstation) {
76         alert(dojo.string.substitute(localeStrings.WORKSTATION_REQUIRED));
77         return;
78     }
79     
80     var self = this;
81     // connect onclick handlers to the various navigation links
82     var linkHandlers = {
83         'oils-selfck-hold-details-link' : function() { self.drawHoldsPage(); },
84         'oils-selfck-nav-holds' : function() { self.drawHoldsPage(); },
85         'oils-selfck-pay-fines-link' : function() { self.drawFinesPage(); },
86         'oils-selfck-nav-fines' : function() { self.drawFinesPage(); },
87         'oils-selfck-nav-home' : function() { self.drawCircPage(); },
88         'oils-selfck-nav-logout' : function() { self.logoutPatron(); }
89     }
90
91     for(var id in linkHandlers) 
92         dojo.connect(dojo.byId(id), 'onclick', linkHandlers[id]);
93
94
95     if(this.cgi.param('patron')) {
96         
97         // Patron barcode via cgi param.  Mainly used for debugging and
98         // only works if password is not required by policy
99         this.loginPatron(this.cgi.param('patron'));
100
101     } else {
102         this.drawLoginPage();
103     }
104 }
105
106 /**
107  * Loads the org unit settings
108  */
109 SelfCheckManager.prototype.loadOrgSettings = function() {
110
111     var settings = fieldmapper.aou.fetchOrgSettingBatch(
112         this.staff.ws_ou(), [
113             SET_BARCODE_REGEX,
114             SET_PATRON_TIMEOUT,
115             SET_ALERT_POPUP,
116             SET_ALERT_SOUND,
117             SET_AUTO_OVERRIDE_EVENTS,
118             SET_PATRON_PASSWORD_REQUIRED,
119             SET_AUTO_RENEW_INTERVAL,
120             SET_WORKSTATION_REQUIRED
121         ]
122     );
123
124     for(k in settings) {
125         if(settings[k])
126             this.orgSettings[k] = settings[k].value;
127     }
128
129     if(settings[SET_BARCODE_REGEX]) 
130         this.patronBarcodeRegex = new RegExp(settings[SET_BARCODE_REGEX].value);
131 }
132
133 SelfCheckManager.prototype.drawLoginPage = function() {
134     var self = this;
135
136     var bcHandler = function(barcode) {
137         // handle patron barcode entry
138
139         if(self.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) {
140             
141             // password is required.  wire up the scan box to read it
142             self.updateScanBox({
143                 msg : 'Please enter your password', // TODO i18n 
144                 handler : function(pw) { self.loginPatron(barcode, pw); },
145                 password : true
146             });
147
148         } else {
149             // password is not required, go ahead and login
150             self.loginPatron(barcode);
151         }
152     };
153
154     this.updateScanBox({
155         msg : 'Please log in with your library barcode.', // TODO
156         handler : bcHandler
157     });
158 }
159
160 /**
161  * Login the patron.  
162  */
163 SelfCheckManager.prototype.loginPatron = function(barcode, passwd) {
164
165     if(this.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) {
166         
167         if(!passwd) {
168             // would only happen in dev/debug mode when using the patron= param
169             alert('password required by org setting.  remove patron= from URL'); 
170             return;
171         }
172
173         // patron password is required.  Verify it.
174
175         var res = fieldmapper.standardRequest(
176             ['open-ils.actor', 'open-ils.actor.verify_user_password'],
177             {params : [this.authtoken, barcode, null, hex_md5(passwd)]}
178         );
179
180         if(res == 0) {
181             // user-not-found results in login failure
182             this.handleAlert(
183                 dojo.string.substitute(localeStrings.LOGIN_FAILED, [barcode]),
184                 false, 'login-failure'
185             );
186             this.drawLoginPage();
187             return;
188         }
189     } 
190
191     // retrieve the fleshed user by barcode
192     this.patron = fieldmapper.standardRequest(
193         ['open-ils.actor', 'open-ils.actor.user.fleshed.retrieve_by_barcode'],
194         {params : [this.authtoken, barcode]}
195     );
196
197     var evt = openils.Event.parse(this.patron);
198     if(evt) {
199         this.handleAlert(
200             dojo.string.substitute(localeStrings.LOGIN_FAILED, [barcode]),
201             false, 'login-failure'
202         );
203         this.drawLoginPage();
204
205     } else {
206
207         this.handleAlert('', false, 'login-success');
208         dojo.byId('oils-selfck-user-banner').innerHTML = 'Welcome, ' + this.patron.usrname(); // TODO i18n
209         this.drawCircPage();
210     }
211 }
212
213
214 SelfCheckManager.prototype.handleAlert = function(message, shouldPopup, sound) {
215
216     console.log("Handling alert " + message);
217
218     dojo.byId('oils-selfck-status-div').innerHTML = message;
219
220     if(shouldPopup && this.orgSettings[SET_ALERT_POPUP]) 
221         alert(message);
222
223     if(sound && this.orgSettings[SET_ALERT_SOUND])
224         openils.Util.playAudioUrl(SelfCheckManager.audioConfig[sound]);
225 }
226
227
228 /**
229  * Manages the main input box
230  * @param msg The context message to display with the box
231  * @param clearOnly Don't update the context message, just clear the value and re-focus
232  * @param handler Optional "on-enter" handler.  
233  */
234 SelfCheckManager.prototype.updateScanBox = function(args) {
235     args = args || {};
236
237     if(args.select) {
238         selfckScanBox.domNode.select();
239     } else {
240         selfckScanBox.attr('value', '');
241     }
242
243     if(args.password) {
244         selfckScanBox.domNode.setAttribute('type', 'password');
245     } else {
246         selfckScanBox.domNode.setAttribute('type', '');
247     }
248
249     if(args.value)
250         selfckScanBox.attr('value', args.value);
251
252     if(args.msg) 
253         dojo.byId('oils-selfck-scan-text').innerHTML = args.msg;
254
255     if(selfckScanBox._lastHandler && (args.handler || args.clearHandler)) {
256         dojo.disconnect(selfckScanBox._lastHandler);
257     }
258
259     if(args.handler) {
260         selfckScanBox._lastHandler = dojo.connect(
261             selfckScanBox, 
262             'onKeyDown', 
263             function(e) {
264                 if(e.keyCode != dojo.keys.ENTER) 
265                     return;
266                 args.handler(selfckScanBox.attr('value'));
267             }
268         );
269     }
270
271     selfckScanBox.focus();
272 }
273
274 /**
275  *  Sets up the checkout/renewal interface
276  */
277 SelfCheckManager.prototype.drawCircPage = function() {
278
279     var self = this;
280     this.updateScanBox({
281         msg : 'Please enter an item barcode', // TODO i18n
282         handler : function(barcode) { self.checkout(barcode); }
283     });
284
285     openils.Util.hide('oils-selfck-payment-page');
286     openils.Util.hide('oils-selfck-holds-page');
287     openils.Util.show('oils-selfck-circ-page');
288
289     this.circTbody = dojo.byId('oils-selfck-circ-tbody');
290     if(!this.circTemplate)
291         this.circTemplate = this.circTbody.removeChild(dojo.byId('oils-selfck-circ-row'));
292
293     // items out, holds, and fines summaries
294
295     // fines summary
296     fieldmapper.standardRequest(
297         ['open-ils.actor', 'open-ils.actor.user.fines.summary'],
298         {   async : true,
299             params : [this.authtoken, this.patron.id()],
300             oncomplete : function(r) {
301                 var summary = openils.Util.readResponse(r);
302                 dojo.byId('oils-selfck-fines-total').innerHTML = 
303                     dojo.string.substitute(
304                         localeStrings.TOTAL_FINES_ACCOUNT, 
305                         [summary.balance_owed()]
306                     );
307             }
308         }
309     );
310
311     // holds summary
312     this.updateHoldsSummary();
313
314     // items out summary
315     this.updateCircSummary();
316
317     // render mock checkouts for debugging?
318     if(this.mockCheckouts) {
319         for(var i in [1,2,3]) 
320             this.displayCheckout(this.mockCheckout, 'checkout');
321     }
322 }
323
324 SelfCheckManager.prototype.updateHoldsSummary = function() {
325
326     if(!this.holdsSummary) {
327         var summary = fieldmapper.standardRequest(
328             ['open-ils.circ', 'open-ils.circ.holds.user_summary'],
329             {params : [this.authtoken, this.patron.id()]}
330         );
331
332         this.holdsSummary = {};
333         this.holdsSummary.ready = Number(summary['4']);
334         this.holdsSummary.total = 0;
335
336         for(var i in summary) 
337             this.holdsSummary.total += Number(summary[i]);
338     }
339
340     dojo.byId('oils-selfck-holds-total').innerHTML = 
341         dojo.string.substitute(
342             localeStrings.TOTAL_HOLDS, 
343             [this.holdsSummary.total]
344         );
345
346     dojo.byId('oils-selfck-holds-ready').innerHTML = 
347         dojo.string.substitute(
348             localeStrings.HOLDS_READY_FOR_PICKUP, 
349             [this.holdsSummary.ready]
350         );
351 }
352
353
354 SelfCheckManager.prototype.updateCircSummary = function(increment) {
355
356     if(!this.circSummary) {
357
358         var summary = fieldmapper.standardRequest(
359             ['open-ils.actor', 'open-ils.actor.user.checked_out.count'],
360             {params : [this.authtoken, this.patron.id()]}
361         );
362
363         this.circSummary = {
364             total : Number(summary.out) + Number(summary.overdue),
365             overdue : Number(summary.overdue),
366             session : 0
367         };
368     }
369
370     if(increment) {
371         // local checkout occurred.  Add to the total and the session.
372         this.circSummary.total += 1;
373         this.circSummary.session += 1;
374     }
375
376     dojo.byId('oils-selfck-circ-account-total').innerHTML = 
377         dojo.string.substitute(
378             localeStrings.TOTAL_ITEMS_ACCOUNT, 
379             [this.circSummary.total]
380         );
381
382     dojo.byId('oils-selfck-circ-session-total').innerHTML = 
383         dojo.string.substitute(
384             localeStrings.TOTAL_ITEMS_SESSION, 
385             [this.circSummary.session]
386         );
387 }
388
389
390 SelfCheckManager.prototype.drawHoldsPage = function() {
391
392     // TODO add option to hid scanBox
393     // this.updateScanBox(...)
394
395     openils.Util.hide('oils-selfck-circ-page');
396     openils.Util.hide('oils-selfck-payment-page');
397     openils.Util.show('oils-selfck-holds-page');
398
399     this.holdTbody = dojo.byId('oils-selfck-hold-tbody');
400     if(!this.holdTemplate)
401         this.holdTemplate = this.holdTbody.removeChild(dojo.byId('oils-selfck-hold-row'));
402     while(this.holdTbody.childNodes[0])
403         this.holdTbody.removeChild(this.holdTbody.childNodes[0]);
404
405     progressDialog.show(true);
406
407     var self = this;
408     fieldmapper.standardRequest( // fetch the hold IDs
409
410         ['open-ils.circ', 'open-ils.circ.holds.id_list.retrieve'],
411         {   async : true,
412             params : [this.authtoken, this.patron.id()],
413
414             oncomplete : function(r) { 
415                 var ids = openils.Util.readResponse(r);
416                 if(!ids || ids.length == 0) {
417                     progressDialog.hide();
418                     return;
419                 }
420
421                 fieldmapper.standardRequest( // fetch the hold objects with fleshed details
422                     ['open-ils.circ', 'open-ils.circ.hold.details.batch.retrieve.atomic'],
423                     {   async : true,
424                         params : [self.authtoken, ids],
425
426                         oncomplete : function(rr) {
427                             self.drawHolds(openils.Util.readResponse(rr));
428                         }
429                     }
430                 );
431             }
432         }
433     );
434 }
435
436 /**
437  * Fetch and add a single hold to the list of holds
438  */
439 SelfCheckManager.prototype.drawHolds = function(holds) {
440
441     holds = holds.sort(
442         // sort available holds to the top of the list
443         // followed by queue position order
444         function(a, b) {
445             if(a.status == 4) return -1;
446             if(a.queue_position < b.queue_position) return -1;
447             return 1;
448         }
449     );
450
451     progressDialog.hide();
452
453     for(var i in holds) {
454
455         var data = holds[i];
456         var row = this.holdTemplate.cloneNode(true);
457
458         if(data.mvr.isbn()) {
459             this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + data.mvr.isbn());
460         }
461
462         this.byName(row, 'title').innerHTML = data.mvr.title();
463         this.byName(row, 'author').innerHTML = data.mvr.author();
464
465         if(data.status == 4) {
466
467             // hold is ready for pickup
468             this.byName(row, 'status').innerHTML = localeStrings.HOLD_STATUS_READY;
469
470         } else {
471
472             // hold is still pending
473             this.byName(row, 'status').innerHTML = 
474                 dojo.string.substitute(
475                     localeStrings.HOLD_STATUS_WAITING,
476                     [data.queue_position, data.potential_copies]
477                 );
478         }
479
480         this.holdTbody.appendChild(row);
481     }
482 }
483
484
485
486 /**
487  * Check out a single item.  If the item is already checked 
488  * out to the patron, redirect to renew()
489  */
490 SelfCheckManager.prototype.checkout = function(barcode, override) {
491
492     if(!barcode) {
493         this.updateScanbox(null, true);
494         return;
495     }
496
497     if(this.mockCheckouts) {
498         // if we're in mock-checkout mode, just insert another
499         // fake circ into the table and get out of here.
500         this.displayCheckout(this.mockCheckout, 'checkout');
501         return;
502     }
503
504     // TODO see if it's a patron barcode
505     // TODO see if this item has already been checked out in this session
506
507     var method = 'open-ils.circ.checkout.full';
508     if(override) method += '.override';
509
510     console.log("Checkout out item " + barcode + " with method " + method);
511
512     var result = fieldmapper.standardRequest(
513         ['open-ils.circ', 'open-ils.circ.checkout.full'],
514         {params: [
515             this.authtoken, {
516                 patron_id : this.patron.id(),
517                 copy_barcode : barcode
518             }
519         ]}
520     );
521
522     console.log(js2JSON(result));
523
524     var stat = this.handleXactResult('checkout', barcode, result);
525
526     if(stat.override) {
527         this.checkout(barcode, true);
528     } else if(stat.renew) {
529         this.renew(barcode);
530     }
531 }
532
533
534 SelfCheckManager.prototype.handleXactResult = function(action, item, result) {
535
536     var displayText = '';
537
538     // If true, the display message is important enough to pop up.  Whether or not
539     // an alert() actually occurs, depends on org unit settings
540     var popup = false;  
541
542     var sound = '';
543
544     // TODO handle lost/missing/etc checkin+checkout override steps
545     
546     var payload = result.payload || {};
547         
548     if(result.textcode == 'NO_SESSION') {
549
550         return this.logoutStaff();
551
552     } else if(result.textcode == 'SUCCESS') {
553
554         if(action == 'checkout') {
555
556             displayText = dojo.string.substitute(localeStrings.CHECKOUT_SUCCESS, [item]);
557             this.displayCheckout(result, 'checkout');
558
559             if(payload.holds_fulfilled && payload.holds_fulfilled.length) {
560                 // A hold was fulfilled, update the hold numbers in the circ summary
561                 console.log("fulfilled hold " + payload.holds_fulfilled + " during checkout");
562                 this.holdsSummary = null;
563                 this.updateHoldsSummary();
564             }
565
566             this.updateCircSummary(true);
567
568         } else if(action == 'renew') {
569
570             displayText = dojo.string.substitute(localeStrings.RENEW_SUCCESS, [item]);
571             this.displayCheckout(result, 'renew');
572         }
573
574         sound = 'checkout-success';
575         this.updateScanBox();
576
577     } else if(result.textcode == 'OPEN_CIRCULATION_EXISTS' && action == 'checkout') {
578
579         // Server says the item is already checked out.  If it's checked out to the
580         // current user, we may need to renew it.  
581
582         if(payload.old_circ) { 
583
584             /*
585             old_circ refers to the previous checkout IFF it's for the same user. 
586             If no auto-renew interval is not defined, assume we should renew it
587             If an auto-renew interval is defined and the payload comes back with
588             auto_renew set to true, do the renewal.  Otherwise, let the patron know
589             the item is already checked out to them.  */
590
591             if( !this.orgSettings[SET_AUTO_RENEW_INTERVAL] ||
592                 (this.orgSettings[SET_AUTO_RENEW_INTERVAL] && payload.auto_renew) ) {
593                 return { renew : true };
594             }
595
596             popup = true;
597             sound = 'checkout-failure';
598             displayText = dojo.string.substitute(localeStrings.ALREADY_OUT, [item]);
599
600         } else {
601             
602             // item is checked out to some other user
603             popup = true;
604             sound = 'checkout-failure';
605             displayText = dojo.string.substitute(localeStrings.OPEN_CIRCULATION_EXISTS, [item]);
606         }
607
608         this.updateScanBox({select:true});
609
610     } else {
611
612         var overrideEvents = this.orgSettings[SET_AUTO_OVERRIDE_EVENTS];
613     
614         if(overrideEvents && overrideEvents.length) {
615             
616             // see if the events we received are all in the list of
617             // events to override
618     
619             if(!result.length) result = [result];
620     
621             var override = true;
622             for(var i = 0; i < result.length; i++) {
623                 var match = overrideEvents.filter(
624                     function(e) { return (e == result[i].textcode); })[0];
625                 if(!match) {
626                     override = false;
627                     break;
628                 }
629             }
630
631             if(override) 
632                 return { override : true };
633         }
634     
635         this.updateScanBox({select : true});
636         popup = true;
637         sound = 'checkout-failure';
638
639         if(result.length) 
640             result = result[0];
641
642         switch(result.textcode) {
643
644             case 'MAX_RENEWALS_REACHED' :
645                 displayText = dojo.string.substitute(
646                     localeStrings.MAX_RENEWALS, [item]);
647                 break;
648
649             case 'ITEM_NOT_CATALOGED' :
650                 displayText = dojo.string.substitute(
651                     localeStrings.ITEM_NOT_CATALOGED, [item]);
652                 break;
653
654             case 'OPEN_CIRCULATION_EXISTS' :
655                 displayText = dojo.string.substitute(
656                     localeStrings.OPEN_CIRCULATION_EXISTS, [item]);
657                 break;
658
659             default:
660                 console.error('Unhandled event ' + result.textcode);
661
662                 if(action == 'checkout' || action == 'renew') {
663                     displayText = dojo.string.substitute(
664                         localeStrings.GENERIC_CIRC_FAILURE, [item]);
665                 } else {
666                     displayText = dojo.string.substitute(
667                         localeStrings.UNKNOWN_ERROR, [result.textcode]);
668                 }
669         }
670     }
671
672     this.handleAlert(displayText, popup, sound);
673     return {};
674 }
675
676
677 /**
678  * Renew an item
679  */
680 SelfCheckManager.prototype.renew = function(barcode, override) {
681
682     var method = 'open-ils.circ.renew';
683     if(override) method += '.override';
684
685     console.log("Renewing item " + barcode + " with method " + method);
686
687     var result = fieldmapper.standardRequest(
688         ['open-ils.circ', method],
689         {params: [
690             this.authtoken, {
691                 patron_id : this.patron.id(),
692                 copy_barcode : barcode
693             }
694         ]}
695     );
696
697     console.log(js2JSON(result));
698
699     var stat = this.handleXactResult('renew', barcode, result);
700
701     if(stat.override)
702         this.renew(barcode, true);
703 }
704
705 /**
706  * Display the result of a checkout or renewal in the items out table
707  */
708 SelfCheckManager.prototype.displayCheckout = function(evt, type) {
709
710     var copy = evt.payload.copy;
711     var record = evt.payload.record;
712     var circ = evt.payload.circ;
713     var row = this.circTemplate.cloneNode(true);
714
715     if(record.isbn()) {
716         this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + record.isbn());
717     }
718
719     this.byName(row, 'barcode').innerHTML = copy.barcode();
720     this.byName(row, 'title').innerHTML = record.title();
721     this.byName(row, 'author').innerHTML = record.author();
722     this.byName(row, 'remaining').innerHTML = circ.renewal_remaining();
723     openils.Util.show(this.byName(row, type));
724
725     var date = dojo.date.stamp.fromISOString(circ.due_date());
726     this.byName(row, 'due_date').innerHTML = 
727         dojo.date.locale.format(date, {selector : 'date'});
728
729     // put new circs at the top of the list
730     this.circTbody.insertBefore(row, this.circTbody.getElementsByTagName('tr')[0]);
731 }
732
733
734 SelfCheckManager.prototype.byName = function(node, name) {
735     return dojo.query('[name=' + name+']', node)[0];
736 }
737
738
739 SelfCheckManager.prototype.drawFinesPage = function() {
740
741     openils.Util.hide('oils-selfck-circ-page');
742     openils.Util.hide('oils-selfck-holds-page');
743     openils.Util.show('oils-selfck-payment-page');
744
745 }
746
747 /**
748  * Print a receipt
749  */
750 SelfCheckManager.prototype.printReceipt = function() {
751 }
752
753
754 /**
755  * Logout the patron and return to the login page
756  */
757 SelfCheckManager.prototype.logoutPatron = function() {
758
759     this.patron = null;
760     this.holdsSummary = null;
761     this.circSummary = null;
762
763     this.drawLoginPage();
764 }
765
766
767 /**
768  * Fire up the manager on page load
769  */
770 openils.Util.addOnLoad(
771     function() {
772         new SelfCheckManager().init();
773     }
774 );