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