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