]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/circ/selfcheck/selfcheck.js
LP#1306814: Make use of patron timeout setting for selfcheck
[working/Evergreen.git] / Open-ILS / web / js / ui / default / circ / selfcheck / selfcheck.js
1 dojo.require('dojo.date.locale');
2 dojo.require('dojo.cookie');
3 dojo.require('dojo.date.stamp');
4 dojo.require('dijit.form.CheckBox');
5 dojo.require('dijit.form.NumberSpinner');
6 dojo.require('openils.CGI');
7 dojo.require('openils.Util');
8 dojo.require('openils.User');
9 dojo.require('openils.Event');
10 dojo.require('openils.widget.ProgressDialog');
11 dojo.require('openils.widget.OrgUnitFilteringSelect');
12
13
14 dojo.requireLocalization('openils.circ', 'selfcheck');
15 var localeStrings = dojo.i18n.getLocalization('openils.circ', 'selfcheck');
16
17 // set patron timeout default
18 var patronTimeout = 180000; /* 3 minutes */
19 var timerId = null;
20
21 const SET_BARCODE_REGEX = 'opac.barcode_regex';
22 const SET_PATRON_TIMEOUT = 'circ.selfcheck.patron_login_timeout';
23 const SET_AUTO_OVERRIDE_EVENTS = 'circ.selfcheck.auto_override_checkout_events';
24 const SET_PATRON_PASSWORD_REQUIRED = 'circ.selfcheck.patron_password_required';
25 const SET_AUTO_RENEW_INTERVAL = 'circ.checkout_auto_renew_age';
26 const SET_WORKSTATION_REQUIRED = 'circ.selfcheck.workstation_required';
27 const SET_ALERT_POPUP = 'circ.selfcheck.alert.popup';
28 const SET_ALERT_SOUND = 'circ.selfcheck.alert.sound';
29 const SET_CC_PAYMENT_ALLOWED = 'credit.payments.allow';
30 // This setting only comes into play if COPY_NOT_AVAILABLE is in the SET_AUTO_OVERRIDE_EVENTS list
31 const SET_BLOCK_CHECKOUT_ON_COPY_STATUS = 'circ.selfcheck.block_checkout_on_copy_status';
32
33 // set before the login dialog is rendered
34 openils.User.default_login_agent = 'selfcheck';
35
36 // start the logout timer
37 function selfckStartTimer() {
38     timerId = setTimeout(
39         function() {
40             SelfCheckManager.prototype.logoutPatron();
41         },
42         patronTimeout
43     );
44 }
45
46 // reset the logout timer
47 function selfckResetTimer() {
48     clearTimeout(timerId);
49     selfckStartTimer();
50 }
51
52 function SelfCheckManager() {
53
54     this.cgi = new openils.CGI();
55     this.staff = null; 
56     this.workstation = null;
57     this.authtoken = null;
58
59     this.patron = null; 
60     this.patronBarcodeRegex = null;
61
62     this.checkouts = [];
63     this.itemsOut = [];
64
65     // During renewals, keep track of the ID of the previous circulation. 
66     // Previous circ is used for tracking failed renewals (for receipts).
67     this.prevCirc = null;
68
69     // current item barcode
70     this.itemBarcode = null; 
71
72     // are we currently performing a renewal?
73     this.isRenewal = false; 
74
75     // dict of org unit settings for "here"
76     this.orgSettings = {};
77
78     // Construct a mock checkout for debugging purposes
79     if(this.mockCheckouts = this.cgi.param('mock-circ')) {
80
81         this.mockCheckout = {
82             payload : {
83                 record : new fieldmapper.mvr(),
84                 copy : new fieldmapper.acp(),
85                 circ : new fieldmapper.circ()
86             }
87         };
88
89         this.mockCheckout.payload.record.title('Jazz improvisation for guitar');
90         this.mockCheckout.payload.record.author('Wise, Les');
91         this.mockCheckout.payload.record.isbn('0634033565');
92         this.mockCheckout.payload.copy.barcode('123456789');
93         this.mockCheckout.payload.circ.renewal_remaining(1);
94         this.mockCheckout.payload.circ.parent_circ(1);
95         this.mockCheckout.payload.circ.due_date('2012-12-21');
96     }
97
98     this.initPrinter();
99 }
100
101 SelfCheckManager.prototype.setupStaffLogin = function(verify) {
102
103     if(verify) oilsSetupUser(); 
104     this.staff = openils.User.user;
105     this.workstation = openils.User.workstation;
106     this.authtoken = openils.User.authtoken;
107 }
108
109
110
111 /**
112  * Fetch the org-unit settings, initialize the display, etc.
113  */
114 SelfCheckManager.prototype.init = function() {
115
116     this.setupStaffLogin();
117     this.loadOrgSettings();
118
119     this.circTbody = dojo.byId('oils-selfck-circ-tbody');
120     this.itemsOutTbody = dojo.byId('oils-selfck-circ-out-tbody');
121
122     // workstation is required but none provided
123     if(this.orgSettings[SET_WORKSTATION_REQUIRED] && !this.workstation) {
124         if(confirm(dojo.string.substitute(localeStrings.WORKSTATION_REQUIRED))) {
125             this.registerWorkstation();
126         }
127         return;
128     }
129     
130     var self = this;
131     // connect onclick handlers to the various navigation links
132     var linkHandlers = {
133         'oils-selfck-hold-details-link' : function() { self.drawHoldsPage(); },
134         'oils-selfck-view-fines-link' : function() { self.drawFinesPage(); },
135         'oils-selfck-pay-fines-link' : function() {
136             self.goToTab("payment");
137             self.drawPayFinesPage(
138                 self.patron,
139                 self.getSelectedFinesTotal(),
140                 self.getSelectedFineTransactions(),
141                 function(resp) {
142                     var evt = openils.Event.parse(resp);
143                     if(evt) {
144                         var message = evt + '';
145                         if(evt.textcode == 'CREDIT_PROCESSOR_DECLINED_TRANSACTION' && evt.payload)
146                             message += '\n' + evt.payload.error_message;
147                         if(evt.textcode == 'INVALID_USER_XACT_ID')
148                             message += '\n' + localeStrings.PAYMENT_INVALID_USER_XACT_ID;
149                         self.handleAlert(message, true, 'payment-failure');
150                         return;
151                     }
152
153                     self.patron.last_xact_id(resp.last_xact_id); // update to match latest from server
154                     self.printPaymentReceipt(
155                         resp,
156                         function() {
157                             self.updateFinesSummary();
158                             self.drawFinesPage();
159                         }
160                     );
161                 }
162             );
163         },
164         'oils-selfck-nav-home' : function() { self.drawCircPage(); },
165         'oils-selfck-nav-logout' : function() { self.logoutPatron(); },
166         'oils-selfck-nav-logout-print' : function() { self.logoutPatron(true); },
167         'oils-selfck-items-out-details-link' : function() { self.drawItemsOutPage(); },
168         'oils-selfck-print-list-link' : function() { self.printList(); }
169     }
170
171     for(var id in linkHandlers) 
172         dojo.connect(dojo.byId(id), 'onclick', linkHandlers[id]);
173
174
175     if(this.cgi.param('patron')) {
176         
177         // Patron barcode via cgi param.  Mainly used for debugging and
178         // only works if password is not required by policy
179         this.loginPatron(this.cgi.param('patron'));
180
181     } else {
182         this.drawLoginPage();
183     }
184
185     /**
186      * To test printing, pass a URL param of 'testprint'.  The value for the param
187      * should be a JSON string like so:  [{circ:<circ_id>}, ...]
188      */
189     var testPrint = this.cgi.param('testprint');
190     if(testPrint) {
191         this.checkouts = JSON2js(testPrint);
192         this.printSessionReceipt();
193         this.checkouts = [];
194     }
195 }
196
197
198 SelfCheckManager.prototype.getSelectedFinesTotal = function() {
199     var total = 0;
200     dojo.forEach(
201         dojo.query("[name=selector]", this.finesTbody),
202         function(input) {
203             if(input.checked)
204                 total += Number(input.getAttribute("balance_owed"));
205         }
206     );
207     return total.toFixed(2);
208 };
209
210 SelfCheckManager.prototype.getSelectedFineTransactions = function() {
211     return dojo.query("[name=selector]", this.finesTbody).
212         filter(function (o) { return o.checked }).
213         map(
214             function (o) {
215                 return [
216                     o.getAttribute("xact"),
217                     Number(o.getAttribute("balance_owed")).toFixed(2)
218                 ];
219             }
220         );
221 };
222
223 /**
224  * Registers a new workstion
225  */
226 SelfCheckManager.prototype.registerWorkstation = function() {
227     
228     oilsSelfckWsDialog.show();
229
230     new openils.User().buildPermOrgSelector(
231         'REGISTER_WORKSTATION', 
232         oilsSelfckWsLocSelector, 
233         this.staff.home_ou()
234     );
235
236
237     var self = this;
238     dojo.connect(oilsSelfckWsSubmit, 'onClick', 
239
240         function() {
241             oilsSelfckWsDialog.hide();
242             var name = oilsSelfckWsLocSelector.attr('displayedValue') + '-' + oilsSelfckWsName.attr('value');
243
244             var res = fieldmapper.standardRequest(
245                 ['open-ils.actor', 'open-ils.actor.workstation.register'],
246                 { params : [
247                         self.authtoken, name, oilsSelfckWsLocSelector.attr('value')
248                     ]
249                 }
250             );
251
252             if(evt = openils.Event.parse(res)) {
253                 if(evt.textcode == 'WORKSTATION_NAME_EXISTS') {
254                     if(confirm(localeStrings.WORKSTATION_EXISTS)) {
255                         location.href = location.href.replace(/\?.*/, '') + '?ws=' + name;
256                     } else {
257                         self.registerWorkstation();
258                     }
259                     return;
260                 } else {
261                     alert(evt);
262                 }
263             } else {
264                 location.href = location.href.replace(/\?.*/, '') + '?ws=' + name;
265             }
266         }
267     );
268 }
269
270 /**
271  * Loads the org unit settings
272  */
273 SelfCheckManager.prototype.loadOrgSettings = function() {
274
275     var settings = fieldmapper.aou.fetchOrgSettingBatch(
276         this.staff.ws_ou(), [
277             SET_BARCODE_REGEX,
278             SET_PATRON_TIMEOUT,
279             SET_ALERT_POPUP,
280             SET_ALERT_SOUND,
281             SET_AUTO_OVERRIDE_EVENTS,
282             SET_BLOCK_CHECKOUT_ON_COPY_STATUS,
283             SET_PATRON_PASSWORD_REQUIRED,
284             SET_AUTO_RENEW_INTERVAL,
285             SET_WORKSTATION_REQUIRED,
286             SET_CC_PAYMENT_ALLOWED
287         ]
288     );
289
290     for(k in settings) {
291         if(settings[k])
292             this.orgSettings[k] = settings[k].value;
293     }
294
295     if(settings[SET_BARCODE_REGEX]) 
296         this.patronBarcodeRegex = new RegExp(settings[SET_BARCODE_REGEX].value);
297
298     if(settings[SET_PATRON_TIMEOUT])
299         patronTimeout = parseInt(settings[SET_PATRON_TIMEOUT].value) * 1000;
300 }
301
302 SelfCheckManager.prototype.drawLoginPage = function() {
303     var self = this;
304
305     var bcHandler = function(barcode_or_usrname) {
306         // handle patron barcode/usrname entry
307
308         if(self.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) {
309             
310             // password is required.  wire up the scan box to read it
311             self.updateScanBox({
312                 msg : localeStrings.ENTER_PASSWORD,
313                 handler : function(pw) { self.loginPatron(barcode_or_usrname, pw); },
314                 password : true
315             });
316
317         } else {
318             // password is not required, go ahead and login
319             self.loginPatron(barcode_or_usrname);
320         }
321     };
322
323     this.updateScanBox({
324         msg : localeStrings.PLEASE_LOGIN,
325         handler : bcHandler
326     });
327 }
328
329 /**
330  * Login the patron.  
331  */
332 SelfCheckManager.prototype.loginPatron = function(barcode_or_usrname, passwd) {
333
334     // reset timeout
335     selfckResetTimer();
336
337     this.setupStaffLogin(true); // verify still valid
338
339     var barcode = null;
340     var usrname = null;
341     console.log('testing ' + barcode_or_usrname);
342     if (barcode_or_usrname.match(this.patronBarcodeRegex)) {
343         console.log('barcode');
344         barcode = barcode_or_usrname;
345     } else {
346         console.log('usrname');
347         usrname = barcode_or_usrname;
348     }
349
350     if(this.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) {
351         
352         if(!passwd) {
353             // would only happen in dev/debug mode when using the patron= param
354             alert('password required by org setting.  remove patron= from URL'); 
355             return;
356         }
357
358         // patron password is required.  Verify it.
359
360         var self = this;
361         new openils.User().auth_verify(
362             {   username : usrname, barcode : barcode, 
363                 type : 'opac', passwd : passwd, agent : 'selfcheck' },
364             function(OK) {
365                 if (OK) {
366                     self.fetchPatron(barcode, usrname);
367
368                 } else {
369                     // auth verify failed
370                     self.handleAlert(
371                         dojo.string.substitute(localeStrings.LOGIN_FAILED, [barcode_or_usrname]),
372                         false, 'login-failure'
373                     );
374                     self.drawLoginPage();
375                 }
376             }
377         );
378
379     } else {
380         this.fetchPatron(barcode, usrname);
381     }
382 };
383
384 SelfCheckManager.prototype.fetchPatron = function(barcode, usrname) {
385
386     var patron_id = fieldmapper.standardRequest(
387         ['open-ils.actor', 'open-ils.actor.user.retrieve_id_by_barcode_or_username'],
388         {params : [this.authtoken, barcode, usrname]}
389     );
390
391     // retrieve the fleshed user by id
392     this.patron = fieldmapper.standardRequest(
393         ['open-ils.actor', 'open-ils.actor.user.fleshed.retrieve.authoritative'],
394         {params : [this.authtoken, patron_id]}
395     );
396
397     var evt = openils.Event.parse(this.patron);
398     
399     // verify validity of the card used to log in
400     var inactiveCard = false;
401     if(!evt) {
402         var card;
403         if (barcode) {
404             card = this.patron.cards().filter(
405                 function(c) { return (c.barcode() == barcode); })[0];
406         } else {
407             card = this.patron.card();
408         }
409         inactiveCard = !openils.Util.isTrue(card.active());
410     }
411
412     if(evt || inactiveCard) {
413         this.handleAlert(
414             dojo.string.substitute(localeStrings.LOGIN_FAILED, [barcode || usrname]),
415             false, 'login-failure'
416         );
417         this.drawLoginPage();
418
419     } else {
420
421         this.handleAlert('', false, 'login-success');
422         dojo.byId('oils-selfck-user-banner').innerHTML = 
423             dojo.string.substitute(localeStrings.WELCOME_BANNER, [this.patron.first_given_name()]);
424         this.drawCircPage();
425     }
426 }
427
428
429 SelfCheckManager.prototype.handleAlert = function(message, shouldPopup, sound) {
430
431     console.log("Handling alert " + message);
432
433     dojo.byId('oils-selfck-status-div').innerHTML = message;
434
435     if(shouldPopup)
436         openils.Util.addCSSClass( dojo.byId('oils-selfck-status-div'), 'checkout_failure' );
437     else
438         openils.Util.removeCSSClass( dojo.byId('oils-selfck-status-div'), 'checkout_failure' );
439
440     if(shouldPopup && this.orgSettings[SET_ALERT_POPUP]) 
441         alert(message);
442
443     if(sound && this.orgSettings[SET_ALERT_SOUND])
444         openils.Util.playAudioUrl(SelfCheckManager.audioConfig[sound]);
445 }
446
447
448 /**
449  * Manages the main input box
450  * @param msg The context message to display with the box
451  * @param clearOnly Don't update the context message, just clear the value and re-focus
452  * @param handler Optional "on-enter" handler.  
453  */
454 SelfCheckManager.prototype.updateScanBox = function(args) {
455     args = args || {};
456
457     if(args.select) {
458         selfckScanBox.domNode.select();
459     } else {
460         selfckScanBox.attr('value', '');
461     }
462
463     if(args.password) {
464         selfckScanBox.domNode.setAttribute('type', 'password');
465     } else {
466         selfckScanBox.domNode.setAttribute('type', '');
467     }
468
469     if(args.value)
470         selfckScanBox.attr('value', args.value);
471
472     if(args.msg) 
473         dojo.byId('oils-selfck-scan-text').innerHTML = args.msg;
474
475     if(selfckScanBox._lastHandler && (args.handler || args.clearHandler)) {
476         dojo.disconnect(selfckScanBox._lastHandler);
477     }
478
479     if(args.handler) {
480         selfckScanBox._lastHandler = dojo.connect(
481             selfckScanBox, 
482             'onKeyDown', 
483             function(e) {
484                 if(e.keyCode != dojo.keys.ENTER) 
485                     return;
486                 args.handler(selfckScanBox.attr('value'));
487             }
488         );
489     }
490
491     selfckScanBox.focus();
492 }
493
494 /**
495  *  Sets up the checkout/renewal interface
496  */
497 SelfCheckManager.prototype.drawCircPage = function() {
498
499     openils.Util.show('oils-selfck-circ-tbody', 'table-row-group');
500     this.goToTab('checkout');
501
502     while(this.itemsOutTbody.childNodes[0])
503         this.itemsOutTbody.removeChild(this.itemsOutTbody.childNodes[0]);
504
505     var self = this;
506     this.updateScanBox({
507         msg : localeStrings.ENTER_BARCODE,
508         handler : function(barcode) { self.checkout(barcode); }
509     });
510
511     if(!this.circTemplate)
512         this.circTemplate = this.circTbody.removeChild(dojo.byId('oils-selfck-circ-row'));
513
514     // fines summary
515     this.updateFinesSummary();
516
517     // holds summary
518     this.updateHoldsSummary();
519
520     // items out summary
521     this.updateCircSummary();
522
523     // render mock checkouts for debugging?
524     if(this.mockCheckouts) {
525         for(var i in [1,2,3]) 
526             this.displayCheckout(this.mockCheckout, 'checkout');
527     }
528 }
529
530
531 SelfCheckManager.prototype.updateFinesSummary = function() {
532     var self = this; 
533
534     // fines summary
535     fieldmapper.standardRequest(
536         ['open-ils.actor', 'open-ils.actor.user.fines.summary'],
537         {   async : true,
538             params : [this.authtoken, this.patron.id()],
539             oncomplete : function(r) {
540
541                 var summary = openils.Util.readResponse(r);
542
543                 dojo.byId('oils-selfck-fines-total').innerHTML = 
544                     dojo.string.substitute(
545                         localeStrings.TOTAL_FINES_ACCOUNT, 
546                         ['<b>' + summary.balance_owed() + '</b>']
547                     );
548
549                 self.creditPayableBalance = summary.balance_owed();
550             }
551         }
552     );
553 }
554
555
556 SelfCheckManager.prototype.drawItemsOutPage = function() {
557     openils.Util.hide('oils-selfck-circ-tbody');
558
559     this.goToTab('items_out');
560
561     while(this.itemsOutTbody.childNodes[0])
562         this.itemsOutTbody.removeChild(this.itemsOutTbody.childNodes[0]);
563
564     progressDialog.show(true);
565     
566     var self = this;
567     fieldmapper.standardRequest(
568         ['open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic'],
569         {
570             async : true,
571             params : [this.authtoken, this.patron.id()],
572             oncomplete : function(r) {
573
574                 var resp = openils.Util.readResponse(r);
575
576                 var circs = resp.sort(
577                     function(a, b) {
578                         if(a.circ.due_date() > b.circ.due_date())
579                             return -1;
580                         return 1;
581                     }
582                 );
583
584                 progressDialog.hide();
585
586                 self.itemsOut = [];
587                 dojo.forEach(circs,
588                     function(circ) {
589                         self.itemsOut.push(circ.circ.id());
590                         self.displayCheckout(
591                             {payload : circ}, 
592                             (circ.circ.parent_circ()) ? 'renew' : 'checkout',
593                             true
594                         );
595                     }
596                 );
597             }
598         }
599     );
600 }
601
602
603 SelfCheckManager.prototype.goToTab = function(name) {
604     this.tabName = name;
605
606     openils.Util.hide('oils-selfck-fines-page');
607     openils.Util.hide('oils-selfck-payment-page');
608     openils.Util.hide('oils-selfck-holds-page');
609     openils.Util.hide('oils-selfck-circ-page');
610     openils.Util.hide('oils-selfck-pay-fines-link');
611
612     // reset timeout
613     selfckResetTimer()
614     
615     switch(name) {
616         case 'checkout':
617             openils.Util.show('oils-selfck-circ-page');
618             break;
619         case 'items_out':
620             openils.Util.show('oils-selfck-circ-page');
621             break;
622         case 'holds':
623             openils.Util.show('oils-selfck-holds-page');
624             break;
625         case 'fines':
626             openils.Util.show('oils-selfck-fines-page');
627             break;
628         case 'payment':
629             openils.Util.show('oils-selfck-payment-page');
630             break;
631     }
632 }
633
634
635 SelfCheckManager.prototype.printList = function() {
636     // reset timeout
637     selfckResetTimer()
638
639     switch(this.tabName) {
640         case 'checkout':
641             this.printSessionReceipt();
642             break;
643         case 'items_out':
644             this.printItemsOutReceipt();
645             break;
646         case 'holds':
647             this.printHoldsReceipt();
648             break;
649         case 'fines':
650             this.printFinesReceipt();
651             break;
652     }
653 }
654
655 SelfCheckManager.prototype.updateHoldsSummary = function() {
656
657     if(!this.holdsSummary) {
658         var summary = fieldmapper.standardRequest(
659             ['open-ils.circ', 'open-ils.circ.holds.user_summary'],
660             {params : [this.authtoken, this.patron.id()]}
661         );
662
663         this.holdsSummary = {};
664         this.holdsSummary.ready = Number(summary['4']);
665         this.holdsSummary.total = 0;
666
667         for(var i in summary) 
668             this.holdsSummary.total += Number(summary[i]);
669     }
670
671     dojo.byId('oils-selfck-holds-total').innerHTML = 
672         dojo.string.substitute(
673             localeStrings.TOTAL_HOLDS, 
674             ['<b>' + this.holdsSummary.total + '</b>']
675         );
676
677     dojo.byId('oils-selfck-holds-ready').innerHTML = 
678         dojo.string.substitute(
679             localeStrings.HOLDS_READY_FOR_PICKUP, 
680             ['<b>' + this.holdsSummary.ready + '</b>']
681         );
682 }
683
684
685 SelfCheckManager.prototype.updateCircSummary = function(increment) {
686
687     if(!this.circSummary) {
688
689         var summary = fieldmapper.standardRequest(
690             ['open-ils.actor', 'open-ils.actor.user.checked_out.count'],
691             {params : [this.authtoken, this.patron.id()]}
692         );
693
694         this.circSummary = {
695             total : Number(summary.out) + Number(summary.overdue),
696             overdue : Number(summary.overdue),
697             session : 0
698         };
699     }
700
701     if(increment) {
702         // local checkout occurred.  Add to the total and the session.
703         this.circSummary.total += 1;
704         this.circSummary.session += 1;
705     }
706
707     dojo.byId('oils-selfck-circ-account-total').innerHTML = 
708         dojo.string.substitute(
709             localeStrings.TOTAL_ITEMS_ACCOUNT, 
710             ['<b>' + this.circSummary.total + '</b>']
711         );
712
713     dojo.byId('oils-selfck-circ-session-total').innerHTML = 
714         dojo.string.substitute(
715             localeStrings.TOTAL_ITEMS_SESSION, 
716             ['<b>' + this.circSummary.session + '</b>']
717         );
718 }
719
720
721 SelfCheckManager.prototype.drawHoldsPage = function() {
722
723     // TODO add option to hid scanBox
724     // this.updateScanBox(...)
725
726     this.goToTab('holds');
727
728     this.holdTbody = dojo.byId('oils-selfck-hold-tbody');
729     if(!this.holdTemplate)
730         this.holdTemplate = this.holdTbody.removeChild(dojo.byId('oils-selfck-hold-row'));
731     while(this.holdTbody.childNodes[0])
732         this.holdTbody.removeChild(this.holdTbody.childNodes[0]);
733
734     progressDialog.show(true);
735
736     var self = this;
737     fieldmapper.standardRequest( // fetch the hold IDs
738
739         ['open-ils.circ', 'open-ils.circ.holds.id_list.retrieve'],
740         {   async : true,
741             params : [this.authtoken, this.patron.id()],
742
743             oncomplete : function(r) { 
744                 var ids = openils.Util.readResponse(r);
745                 if(!ids || ids.length == 0) {
746                     progressDialog.hide();
747                     return;
748                 }
749
750                 fieldmapper.standardRequest( // fetch the hold objects with fleshed details
751                     ['open-ils.circ', 'open-ils.circ.hold.details.batch.retrieve'],
752                     {   async : true,
753                         params : [self.authtoken, ids],
754                         onresponse : function(rr) {
755                             progressDialog.hide();
756                             self.insertHold(openils.Util.readResponse(rr));
757                         }
758                     }
759                 );
760             }
761         }
762     );
763 }
764
765 SelfCheckManager.prototype.insertHold = function(data) {
766     var row = this.holdTemplate.cloneNode(true);
767
768     if(data.mvr.isbn()) {
769         this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/r/' + data.mvr.doc_id());
770     }
771
772     this.byName(row, 'title').innerHTML = data.mvr.title();
773     this.byName(row, 'author').innerHTML = data.mvr.author();
774
775     if(data.status == 4) {
776
777         // hold is ready for pickup
778         this.byName(row, 'status').innerHTML = localeStrings.HOLD_STATUS_READY;
779
780     } else {
781
782         // hold is still pending
783         this.byName(row, 'status').innerHTML = 
784             dojo.string.substitute(
785                 localeStrings.HOLD_STATUS_WAITING,
786                 [data.queue_position, data.potential_copies]
787             );
788     }
789
790     // find the correct place the table to slot in the hold based on queue position
791
792     var position = (data.status == 4) ? 0 : data.queue_position;
793     row.setAttribute('position', position);
794
795     for(var i = 0; i < this.holdTbody.childNodes.length; i++) {
796         var node = this.holdTbody.childNodes[i];
797         if(Number(node.getAttribute('position')) >= position) {
798             this.holdTbody.insertBefore(row, node);
799             return;
800         }
801     }
802
803     this.holdTbody.appendChild(row);
804 }
805
806
807 SelfCheckManager.prototype.drawFinesPage = function() {
808
809     // TODO add option to hid scanBox
810     // this.updateScanBox(...)
811
812     this.goToTab('fines');
813     progressDialog.show(true);
814
815     if(this.creditPayableBalance > 0 && this.orgSettings[SET_CC_PAYMENT_ALLOWED]) {
816         openils.Util.show('oils-selfck-pay-fines-link', 'inline');
817     }
818
819     this.finesTbody = dojo.byId('oils-selfck-fines-tbody');
820     if(!this.finesTemplate)
821         this.finesTemplate = this.finesTbody.removeChild(dojo.byId('oils-selfck-fines-row'));
822     while(this.finesTbody.childNodes[0])
823         this.finesTbody.removeChild(this.finesTbody.childNodes[0]);
824
825     // when user clicks on a selector checkbox, update the total owed
826     var updateSelected = function() {
827         var total = 0;
828         dojo.forEach(
829             dojo.query('[name=selector]', this.finesTbody),
830             function(input) {
831                 if(input.checked)
832                     total += Number(input.getAttribute('balance_owed'));
833             }
834         );
835
836         total = total.toFixed(2);
837         dojo.byId('oils-selfck-selected-total').innerHTML = 
838             dojo.string.substitute(localeStrings.TOTAL_FINES_SELECTED, [total]);
839     }
840
841     // wire up the batch on/off selector
842     var sel = dojo.byId('oils-selfck-fines-selector');
843     sel.onchange = function() {
844         dojo.forEach(
845             dojo.query('[name=selector]', this.finesTbody),
846             function(input) {
847                 input.checked = sel.checked;
848             }
849         );
850     };
851
852     var self = this;
853     var handler = function(dataList) {
854
855         self.finesCount = dataList.length;
856         self.finesData = dataList;
857
858         for(var i in dataList) {
859
860             var data = dataList[i];
861             var row = self.finesTemplate.cloneNode(true);
862             var type = data.transaction.xact_type();
863
864             if(type == 'circulation') {
865                 self.byName(row, 'type').innerHTML = type;
866                 self.byName(row, 'details').innerHTML = data.record.title();
867
868             } else if(type == 'grocery') {
869                 self.byName(row, 'type').innerHTML = localeStrings.MISCELLANEOUS; // Go ahead and head off any confusion around "grocery".
870                 self.byName(row, 'details').innerHTML = data.transaction.last_billing_type();
871             }
872
873             self.byName(row, 'total_owed').innerHTML = data.transaction.total_owed();
874             self.byName(row, 'total_paid').innerHTML = data.transaction.total_paid();
875             self.byName(row, 'balance').innerHTML = data.transaction.balance_owed();
876
877             // row selector
878             var selector = self.byName(row, 'selector')
879             selector.onchange = updateSelected;
880             selector.setAttribute('xact', data.transaction.id());
881             selector.setAttribute('balance_owed', data.transaction.balance_owed());
882             selector.checked = true;
883
884             self.finesTbody.appendChild(row);
885         }
886
887         updateSelected();
888     }
889
890
891     fieldmapper.standardRequest( 
892         ['open-ils.actor', 'open-ils.actor.user.transactions.have_balance.fleshed'],
893         {   async : true,
894             params : [this.authtoken, this.patron.id()],
895             oncomplete : function(r) { 
896                 progressDialog.hide();
897                 handler(openils.Util.readResponse(r));
898             }
899         }
900     );
901 }
902
903 SelfCheckManager.prototype.checkin = function(barcode, abortTransit) {
904
905     var resp = fieldmapper.standardRequest(
906         ['open-ils.circ', 'open-ils.circ.transit.abort'],
907         {params : [this.authtoken, {barcode : barcode}]}
908     );
909
910     // resp == 1 on success
911     if(openils.Event.parse(resp))
912         return false;
913
914     var resp = fieldmapper.standardRequest(
915         ['open-ils.circ', 'open-ils.circ.checkin.override'],
916         {params : [
917             this.authtoken, {
918                 patron_id : this.patron.id(),
919                 copy_barcode : barcode,
920                 noop : true
921             }
922         ]}
923     );
924
925     if(!resp.length) resp = [resp];
926     for(var i = 0; i < resp.length; i++) {
927         var tc = openils.Event.parse(resp[i]).textcode;
928         if(tc == 'SUCCESS' || tc == 'NO_CHANGE') {
929             continue;
930         } else {
931             return false;
932         }
933     }
934
935     return true;
936 }
937
938 /**
939  * Check out a single item.  If the item is already checked 
940  * out to the patron, redirect to renew()
941  */
942 SelfCheckManager.prototype.checkout = function(barcode, override) {
943
944     this.prevCirc = null;
945
946     if(!barcode) {
947         this.updateScanbox(null, true);
948         return;
949     }
950
951     if(this.mockCheckouts) {
952         // if we're in mock-checkout mode, just insert another
953         // fake circ into the table and get out of here.
954         this.displayCheckout(this.mockCheckout, 'checkout');
955         return;
956     }
957
958     // TODO see if it's a patron barcode
959     // TODO see if this item has already been checked out in this session
960
961     var method = 'open-ils.circ.checkout.full';
962     if(override) method += '.override';
963
964     console.log("Checkout out item " + barcode + " with method " + method);
965
966     var result = fieldmapper.standardRequest(
967         ['open-ils.circ', method],
968         {params: [
969             this.authtoken, {
970                 patron_id : this.patron.id(),
971                 copy_barcode : barcode
972             }
973         ]}
974     );
975
976     var stat = this.handleXactResult('checkout', barcode, result);
977
978     if(stat.override) {
979         this.checkout(barcode, true);
980     } else if(stat.doOver) {
981         this.checkout(barcode);
982     } else if(stat.renew) {
983         this.renew(barcode);
984     }
985 }
986
987 SelfCheckManager.prototype.failPartMessage = function(result) {
988     if (result.payload && result.payload.fail_part) {
989         var stringKey = "FAIL_PART_" +
990             result.payload.fail_part.replace(/\./g, "_");
991         return localeStrings[stringKey];
992     } else {
993         return null;
994     }
995 }
996
997 SelfCheckManager.prototype.handleXactResult = function(action, item, result) {
998
999     var displayText = '';
1000
1001     // If true, the display message is important enough to pop up.  Whether or not
1002     // an alert() actually occurs, depends on org unit settings
1003     var popup = false;  
1004     var sound = ''; // sound file reference
1005     var payload = result.payload || {};
1006     var overrideEvents = this.orgSettings[SET_AUTO_OVERRIDE_EVENTS];
1007     var blockStatuses = this.orgSettings[SET_BLOCK_CHECKOUT_ON_COPY_STATUS];
1008         
1009     if(result.textcode == 'NO_SESSION') {
1010
1011         return this.logoutStaff();
1012
1013     } else if(result.textcode == 'SUCCESS') {
1014
1015         if(action == 'checkout') {
1016
1017             displayText = dojo.string.substitute(localeStrings.CHECKOUT_SUCCESS, [item]);
1018             this.displayCheckout(result, 'checkout');
1019
1020             if(payload.holds_fulfilled && payload.holds_fulfilled.length) {
1021                 // A hold was fulfilled, update the hold numbers in the circ summary
1022                 console.log("fulfilled hold " + payload.holds_fulfilled + " during checkout");
1023                 this.holdsSummary = null;
1024                 this.updateHoldsSummary();
1025             }
1026
1027             this.updateCircSummary(true);
1028
1029         } else if(action == 'renew') {
1030
1031             displayText = dojo.string.substitute(localeStrings.RENEW_SUCCESS, [item]);
1032             this.displayCheckout(result, 'renew');
1033         }
1034
1035         this.checkouts.push({circ : result.payload.circ.id()});
1036         sound = 'checkout-success';
1037         this.updateScanBox();
1038
1039     } else if(result.textcode == 'OPEN_CIRCULATION_EXISTS' && action == 'checkout') {
1040
1041         // Server says the item is already checked out.  If it's checked out to the
1042         // current user, we may need to renew it.  
1043
1044         if(payload.old_circ) { 
1045
1046             /*
1047             old_circ refers to the previous checkout IFF it's for the same user. 
1048             If no auto-renew interval is not defined, assume we should renew it
1049             If an auto-renew interval is defined and the payload comes back with
1050             auto_renew set to true, do the renewal.  Otherwise, let the patron know
1051             the item is already checked out to them.  */
1052
1053             if( !this.orgSettings[SET_AUTO_RENEW_INTERVAL] ||
1054                 (this.orgSettings[SET_AUTO_RENEW_INTERVAL] && payload.auto_renew) ) {
1055                 this.prevCirc = payload.old_circ.id();
1056                 return { renew : true };
1057             }
1058
1059             popup = true;
1060             sound = 'checkout-failure';
1061             displayText = dojo.string.substitute(localeStrings.ALREADY_OUT, [item]);
1062
1063         } else {
1064
1065             if( // copy is marked lost.  if configured to do so, check it in and try again.
1066                 result.payload.copy && 
1067                 result.payload.copy.status() == /* LOST */ 3 &&
1068                 overrideEvents && overrideEvents.length &&
1069                 overrideEvents.indexOf('COPY_STATUS_LOST') != -1) {
1070
1071                     if(this.checkin(item)) {
1072                         return { doOver : true };
1073                     }
1074             }
1075
1076             
1077             // item is checked out to some other user
1078             popup = true;
1079             sound = 'checkout-failure';
1080             displayText = dojo.string.substitute(localeStrings.OPEN_CIRCULATION_EXISTS, [item]);
1081         }
1082
1083         this.updateScanBox({select:true});
1084
1085     } else {
1086
1087     
1088         if(overrideEvents && overrideEvents.length) {
1089             
1090             // see if the events we received are all in the list of
1091             // events to override
1092     
1093             if(!result.length) result = [result];
1094     
1095             var override = true;
1096             for(var i = 0; i < result.length; i++) {
1097
1098                 var match = overrideEvents.filter(function(e) { return (e == result[i].textcode); })[0];
1099
1100                 if(!match) {
1101                     override = false;
1102                     break;
1103                 }
1104
1105                 if(result[i].textcode == 'COPY_NOT_AVAILABLE' && blockStatuses && blockStatuses.length) {
1106
1107                     var stat = result[i].payload.status(); // copy status
1108                     if(typeof stat == 'object') stat = stat.id();
1109
1110                     var match2 = blockStatuses.filter(function(e) { return (e == stat); })[0];
1111
1112                     if(match2) { // copy is in a blocked status
1113                         override = false;
1114                         break;
1115                     }
1116                 }
1117
1118                 if(result[i].textcode == 'COPY_IN_TRANSIT') {
1119                     // to override a transit, we have to abort the transit and check it in first
1120                     if(this.checkin(item, true)) {
1121                         return { doOver : true };
1122                     } else {
1123                         override = false;
1124                     }
1125                 }
1126             }
1127
1128             if(override) 
1129                 return { override : true };
1130         }
1131     
1132         this.updateScanBox({select : true});
1133         popup = true;
1134         sound = 'checkout-failure';
1135
1136         if(action == 'renew')
1137             this.checkouts.push({circ : this.prevCirc, renewal_failure : true});
1138
1139         if(result.length) 
1140             result = result[0];
1141
1142         switch(result.textcode) {
1143
1144             // TODO custom handler for blocking penalties
1145
1146             case 'MAX_RENEWALS_REACHED' :
1147                 displayText = dojo.string.substitute(
1148                     localeStrings.MAX_RENEWALS, [item]);
1149                 break;
1150
1151             case 'ITEM_NOT_CATALOGED' :
1152                 displayText = dojo.string.substitute(
1153                     localeStrings.ITEM_NOT_CATALOGED, [item]);
1154                 break;
1155
1156             case 'OPEN_CIRCULATION_EXISTS' :
1157                 displayText = dojo.string.substitute(
1158                     localeStrings.OPEN_CIRCULATION_EXISTS, [item]);
1159
1160                 break;
1161
1162             default:
1163                 console.error('Unhandled event ' + result.textcode);
1164
1165                 if (!(displayText = this.failPartMessage(result))) {
1166                     if (action == 'checkout' || action == 'renew') {
1167                         displayText = dojo.string.substitute(
1168                             localeStrings.GENERIC_CIRC_FAILURE, [item]);
1169                     } else {
1170                         displayText = dojo.string.substitute(
1171                             localeStrings.UNKNOWN_ERROR, [result.textcode]);
1172                     }
1173                 }
1174         }
1175     }
1176
1177     this.handleAlert(displayText, popup, sound);
1178     return {};
1179 }
1180
1181
1182 /**
1183  * Renew an item
1184  */
1185 SelfCheckManager.prototype.renew = function(barcode, override) {
1186
1187     var method = 'open-ils.circ.renew';
1188     if(override) method += '.override';
1189
1190     console.log("Renewing item " + barcode + " with method " + method);
1191
1192     var result = fieldmapper.standardRequest(
1193         ['open-ils.circ', method],
1194         {params: [
1195             this.authtoken, {
1196                 patron_id : this.patron.id(),
1197                 copy_barcode : barcode
1198             }
1199         ]}
1200     );
1201
1202     console.log(js2JSON(result));
1203
1204     var stat = this.handleXactResult('renew', barcode, result);
1205
1206     if(stat.override)
1207         this.renew(barcode, true);
1208 }
1209
1210 /**
1211  * Display the result of a checkout or renewal in the items out table
1212  */
1213 SelfCheckManager.prototype.displayCheckout = function(evt, type, itemsOut) {
1214
1215     var copy = evt.payload.copy;
1216     var record = evt.payload.record;
1217     var circ = evt.payload.circ;
1218     var row = this.circTemplate.cloneNode(true);
1219
1220     if(record.doc_id()) {
1221         this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/r/' + record.doc_id());
1222     }
1223
1224     this.byName(row, 'barcode').innerHTML = copy.barcode();
1225     this.byName(row, 'title').innerHTML = record.title();
1226     this.byName(row, 'author').innerHTML = record.author();
1227     this.byName(row, 'remaining').innerHTML = circ.renewal_remaining();
1228     openils.Util.show(this.byName(row, type));
1229
1230     var date = dojo.date.stamp.fromISOString(circ.due_date());
1231     this.byName(row, 'due_date').innerHTML = 
1232         dojo.date.locale.format(date, {selector : 'date'});
1233
1234     // put new circs at the top of the list
1235     var tbody = this.circTbody;
1236     if(itemsOut) tbody = this.itemsOutTbody;
1237     tbody.insertBefore(row, tbody.getElementsByTagName('tr')[0]);
1238 }
1239
1240
1241 SelfCheckManager.prototype.byName = function(node, name) {
1242     return dojo.query('[name=' + name+']', node)[0];
1243 }
1244
1245
1246 SelfCheckManager.prototype.initPrinter = function() {
1247     try { // Mozilla only
1248                 netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
1249         netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
1250         netscape.security.PrivilegeManager.enablePrivilege('UniversalPreferencesRead');
1251         netscape.security.PrivilegeManager.enablePrivilege('UniversalPreferencesWrite');
1252         var pref = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
1253         if (pref)
1254             pref.setBoolPref('print.always_print_silent', true);
1255     } catch(E) {
1256         console.log("Unable to initialize auto-printing"); 
1257     }
1258 }
1259
1260 /**
1261  * Print a receipt for this session's checkouts
1262  */
1263 SelfCheckManager.prototype.printSessionReceipt = function(callback) {
1264
1265     var circIds = [];
1266     var circCtx = []; // circ context data.  in this case, renewal_failure info
1267
1268     // collect the circs and failure info
1269     dojo.forEach(
1270         this.checkouts, 
1271         function(blob) {
1272             circIds.push(blob.circ);
1273             circCtx.push({renewal_failure:blob.renewal_failure});
1274         }
1275     );
1276
1277     var params = [
1278         this.authtoken, 
1279         this.staff.ws_ou(),
1280         null,
1281         'format.selfcheck.checkout',
1282         'print-on-demand',
1283         circIds,
1284         circCtx
1285     ];
1286
1287     var self = this;
1288     fieldmapper.standardRequest(
1289         ['open-ils.circ', 'open-ils.circ.fire_circ_trigger_events'],
1290         {   
1291             async : true,
1292             params : params,
1293             oncomplete : function(r) {
1294                 var resp = openils.Util.readResponse(r);
1295                 var output = resp.template_output();
1296                 if(output) {
1297                     self.printData(output.data(), self.checkouts.length, callback); 
1298                 } else {
1299                     var error = resp.error_output();
1300                     if(error) {
1301                         throw new Error("Error creating receipt: " + error.data());
1302                     } else {
1303                         throw new Error("No receipt data returned from server");
1304                     }
1305                 }
1306             }
1307         }
1308     );
1309 }
1310
1311 SelfCheckManager.prototype.printData = function(data, numItems, callback) {
1312
1313     var win = window.open('', '', 'resizable,width=700,height=500,scrollbars=1,chrome'); 
1314     win.document.body.innerHTML = data;
1315     win.print();
1316
1317     /*
1318      * There is no way to know when the browser is done printing.
1319      * Make a best guess at when to close the print window by basing
1320      * the setTimeout wait on the number of items to be printed plus
1321      * a small buffer
1322      */
1323     var sleepTime = 1000;
1324     if(numItems > 0) 
1325         sleepTime += (numItems / 2) * 1000;
1326
1327     setTimeout(
1328         function() { 
1329             win.close(); // close the print window
1330             if(callback)
1331                 callback(); // fire optional post-print callback
1332         },
1333         sleepTime 
1334     );
1335 }
1336
1337
1338 /**
1339  * Print a receipt for this user's items out
1340  */
1341 SelfCheckManager.prototype.printItemsOutReceipt = function(callback) {
1342
1343     if(!this.itemsOut.length) return;
1344
1345     progressDialog.show(true);
1346
1347     var params = [
1348         this.authtoken, 
1349         this.staff.ws_ou(),
1350         null,
1351         'format.selfcheck.items_out',
1352         'print-on-demand',
1353         this.itemsOut
1354     ];
1355
1356     var self = this;
1357     fieldmapper.standardRequest(
1358         ['open-ils.circ', 'open-ils.circ.fire_circ_trigger_events'],
1359         {   
1360             async : true,
1361             params : params,
1362             oncomplete : function(r) {
1363                 progressDialog.hide();
1364                 var resp = openils.Util.readResponse(r);
1365                 var output = resp.template_output();
1366                 if(output) {
1367                     self.printData(output.data(), self.itemsOut.length, callback); 
1368                 } else {
1369                     var error = resp.error_output();
1370                     if(error) {
1371                         throw new Error("Error creating receipt: " + error.data());
1372                     } else {
1373                         throw new Error("No receipt data returned from server");
1374                     }
1375                 }
1376             }
1377         }
1378     );
1379 }
1380
1381 /**
1382  * Print a receipt for this user's items out
1383  */
1384 SelfCheckManager.prototype.printHoldsReceipt = function(callback) {
1385
1386     if(!this.holds.length) return;
1387
1388     progressDialog.show(true);
1389
1390     var holdIds = [];
1391     var holdData = [];
1392
1393     dojo.forEach(this.holds,
1394         function(data) {
1395             holdIds.push(data.hold.id());
1396             if(data.status == 4) {
1397                 holdData.push({ready : true});
1398             } else {
1399                 holdData.push({
1400                     queue_position : data.queue_position, 
1401                     potential_copies : data.potential_copies
1402                 });
1403             }
1404         }
1405     );
1406
1407     var params = [
1408         this.authtoken, 
1409         this.staff.ws_ou(),
1410         null,
1411         'format.selfcheck.holds',
1412         'print-on-demand',
1413         holdIds,
1414         holdData
1415     ];
1416
1417     var self = this;
1418     fieldmapper.standardRequest(
1419         ['open-ils.circ', 'open-ils.circ.fire_hold_trigger_events'],
1420         {   
1421             async : true,
1422             params : params,
1423             oncomplete : function(r) {
1424                 progressDialog.hide();
1425                 var resp = openils.Util.readResponse(r);
1426                 var output = resp.template_output();
1427                 if(output) {
1428                     self.printData(output.data(), self.holds.length, callback); 
1429                 } else {
1430                     var error = resp.error_output();
1431                     if(error) {
1432                         throw new Error("Error creating receipt: " + error.data());
1433                     } else {
1434                         throw new Error("No receipt data returned from server");
1435                     }
1436                 }
1437             }
1438         }
1439     );
1440 }
1441
1442
1443 SelfCheckManager.prototype.printPaymentReceipt = function(response, callback) {
1444     
1445     var self = this;
1446     progressDialog.show(true);
1447
1448     fieldmapper.standardRequest(
1449         ['open-ils.circ', 'open-ils.circ.money.payment_receipt.print'],
1450         {
1451             async : true,
1452             params : [this.authtoken, response.payments],
1453             oncomplete : function(r) {
1454                 var resp = openils.Util.readResponse(r);
1455                 var output = resp.template_output();
1456                 progressDialog.hide();
1457                 if(output) {
1458                     self.printData(output.data(), 1, callback); 
1459                 } else {
1460                     var error = resp.error_output();
1461                     if(error) {
1462                         throw new Error("Error creating receipt: " + error.data());
1463                     } else {
1464                         throw new Error("No receipt data returned from server");
1465                     }
1466                 }
1467             }
1468         }
1469     );
1470 }
1471
1472 /**
1473  * Print a receipt for this user's items out
1474  */
1475 SelfCheckManager.prototype.printFinesReceipt = function(callback) {
1476
1477     progressDialog.show(true);
1478
1479     var params = [
1480         this.authtoken, 
1481         this.staff.ws_ou(),
1482         null,
1483         'format.selfcheck.fines',
1484         'print-on-demand',
1485         [this.patron.id()]
1486     ];
1487
1488     var self = this;
1489     fieldmapper.standardRequest(
1490         ['open-ils.circ', 'open-ils.circ.fire_user_trigger_events'],
1491         {   
1492             async : true,
1493             params : params,
1494             oncomplete : function(r) {
1495                 progressDialog.hide();
1496                 var resp = openils.Util.readResponse(r);
1497                 var output = resp.template_output();
1498                 if(output) {
1499                     self.printData(output.data(), self.finesCount, callback); 
1500                 } else {
1501                     var error = resp.error_output();
1502                     if(error) {
1503                         throw new Error("Error creating receipt: " + error.data());
1504                     } else {
1505                         throw new Error("No receipt data returned from server");
1506                     }
1507                 }
1508             }
1509         }
1510     );
1511 }
1512
1513
1514
1515
1516 /**
1517  * Logout the patron and return to the login page
1518  */
1519 SelfCheckManager.prototype.logoutPatron = function(print) {
1520     progressDialog.show(true); // prevent patron from clicking logout link twice
1521     if(print && this.checkouts.length) {
1522         this.printSessionReceipt(
1523             function() {
1524                 location.href = location.href;
1525             }
1526         );
1527     } else {
1528         location.href = location.href;
1529     }
1530 }
1531
1532
1533 /**
1534  * Fire up the manager on page load
1535  */
1536 openils.Util.addOnLoad(
1537     function() {
1538         new SelfCheckManager().init();
1539     }
1540 );