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