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