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