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