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