]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/circ/selfcheck/selfcheck.js
added sample selfcheck fines receipt and plugged in fines printing from self-check UI
[working/Evergreen.git] / Open-ILS / web / js / ui / default / circ / selfcheck / selfcheck.js
1 dojo.require('dojo.date.locale');
2 dojo.require('dojo.date.stamp');
3 dojo.require('openils.CGI');
4 dojo.require('openils.Util');
5 dojo.require('openils.User');
6 dojo.require('openils.Event');
7 dojo.require('openils.widget.ProgressDialog');
8
9 dojo.requireLocalization('openils.circ', 'selfcheck');
10 var localeStrings = dojo.i18n.getLocalization('openils.circ', 'selfcheck');
11
12
13 const SET_BARCODE_REGEX = 'opac.barcode_regex';
14 const SET_PATRON_TIMEOUT = 'circ.selfcheck.patron_login_timeout';
15 const SET_AUTO_OVERRIDE_EVENTS = 'circ.selfcheck.auto_override_checkout_events';
16 const SET_PATRON_PASSWORD_REQUIRED = 'circ.selfcheck.patron_password_required';
17 const SET_AUTO_RENEW_INTERVAL = 'circ.checkout_auto_renew_age';
18 const SET_WORKSTATION_REQUIRED = 'circ.selfcheck.workstation_required';
19 const SET_ALERT_POPUP = 'circ.selfcheck.alert.popup';
20 const SET_ALERT_SOUND = 'circ.selfcheck.alert.sound';
21
22 function SelfCheckManager() {
23
24     this.cgi = new openils.CGI();
25     this.staff = null; 
26     this.workstation = null;
27     this.authtoken = null;
28
29     this.patron = null; 
30     this.patronBarcodeRegex = null;
31
32     this.checkouts = [];
33     this.itemsOut = [];
34
35     // During renewals, keep track of the ID of the previous circulation. 
36     // Previous circ is used for tracking failed renewals (for receipts).
37     this.prevCirc = null;
38
39     // current item barcode
40     this.itemBarcode = null; 
41
42     // are we currently performing a renewal?
43     this.isRenewal = false; 
44
45     // dict of org unit settings for "here"
46     this.orgSettings = {};
47
48     // Construct a mock checkout for debugging purposes
49     if(this.mockCheckouts = this.cgi.param('mock-circ')) {
50
51         this.mockCheckout = {
52             payload : {
53                 record : new fieldmapper.mvr(),
54                 copy : new fieldmapper.acp(),
55                 circ : new fieldmapper.circ()
56             }
57         };
58
59         this.mockCheckout.payload.record.title('Jazz improvisation for guitar');
60         this.mockCheckout.payload.record.author('Wise, Les');
61         this.mockCheckout.payload.record.isbn('0634033565');
62         this.mockCheckout.payload.copy.barcode('123456789');
63         this.mockCheckout.payload.circ.renewal_remaining(1);
64         this.mockCheckout.payload.circ.parent_circ(1);
65         this.mockCheckout.payload.circ.due_date('2012-12-21');
66     }
67 }
68
69
70
71 /**
72  * Fetch the org-unit settings, initialize the display, etc.
73  */
74 SelfCheckManager.prototype.init = function() {
75
76     this.staff = openils.User.user;
77     this.workstation = openils.User.workstation;
78     this.authtoken = openils.User.authtoken;
79     this.loadOrgSettings();
80
81     this.circTbody = dojo.byId('oils-selfck-circ-tbody');
82     this.itemsOutTbody = dojo.byId('oils-selfck-circ-out-tbody');
83
84     // workstation is required but none provided
85     if(this.orgSettings[SET_WORKSTATION_REQUIRED] && !this.workstation) {
86         alert(dojo.string.substitute(localeStrings.WORKSTATION_REQUIRED));
87         return;
88     }
89     
90     var self = this;
91     // connect onclick handlers to the various navigation links
92     var linkHandlers = {
93         'oils-selfck-hold-details-link' : function() { self.drawHoldsPage(); },
94         'oils-selfck-pay-fines-link' : function() { self.drawFinesPage(); },
95         'oils-selfck-nav-home' : function() { self.drawCircPage(); },
96         'oils-selfck-nav-logout' : function() { self.logoutPatron(); },
97         'oils-selfck-nav-logout-print' : function() { self.logoutPatron(true); },
98         'oils-selfck-items-out-details-link' : function() { self.drawItemsOutPage(); },
99         'oils-selfck-print-list-link' : function() { self.printList(); }
100     }
101
102     for(var id in linkHandlers) 
103         dojo.connect(dojo.byId(id), 'onclick', linkHandlers[id]);
104
105
106     if(this.cgi.param('patron')) {
107         
108         // Patron barcode via cgi param.  Mainly used for debugging and
109         // only works if password is not required by policy
110         this.loginPatron(this.cgi.param('patron'));
111
112     } else {
113         this.drawLoginPage();
114     }
115
116     /**
117      * To test printing, pass a URL param of 'testprint'.  The value for the param
118      * should be a JSON string like so:  [{circ:<circ_id>}, ...]
119      */
120     var testPrint = this.cgi.param('testprint');
121     if(testPrint) {
122         this.checkouts = JSON2js(testPrint);
123         this.printSessionReceipt();
124         this.checkouts = [];
125     }
126 }
127
128 /**
129  * Loads the org unit settings
130  */
131 SelfCheckManager.prototype.loadOrgSettings = function() {
132
133     var settings = fieldmapper.aou.fetchOrgSettingBatch(
134         this.staff.ws_ou(), [
135             SET_BARCODE_REGEX,
136             SET_PATRON_TIMEOUT,
137             SET_ALERT_POPUP,
138             SET_ALERT_SOUND,
139             SET_AUTO_OVERRIDE_EVENTS,
140             SET_PATRON_PASSWORD_REQUIRED,
141             SET_AUTO_RENEW_INTERVAL,
142             SET_WORKSTATION_REQUIRED
143         ]
144     );
145
146     for(k in settings) {
147         if(settings[k])
148             this.orgSettings[k] = settings[k].value;
149     }
150
151     if(settings[SET_BARCODE_REGEX]) 
152         this.patronBarcodeRegex = new RegExp(settings[SET_BARCODE_REGEX].value);
153 }
154
155 SelfCheckManager.prototype.drawLoginPage = function() {
156     var self = this;
157
158     var bcHandler = function(barcode) {
159         // handle patron barcode entry
160
161         if(self.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) {
162             
163             // password is required.  wire up the scan box to read it
164             self.updateScanBox({
165                 msg : 'Please enter your password', // TODO i18n 
166                 handler : function(pw) { self.loginPatron(barcode, pw); },
167                 password : true
168             });
169
170         } else {
171             // password is not required, go ahead and login
172             self.loginPatron(barcode);
173         }
174     };
175
176     this.updateScanBox({
177         msg : 'Please log in with your library barcode.', // TODO
178         handler : bcHandler
179     });
180 }
181
182 /**
183  * Login the patron.  
184  */
185 SelfCheckManager.prototype.loginPatron = function(barcode, passwd) {
186
187     if(this.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) {
188         
189         if(!passwd) {
190             // would only happen in dev/debug mode when using the patron= param
191             alert('password required by org setting.  remove patron= from URL'); 
192             return;
193         }
194
195         // patron password is required.  Verify it.
196
197         var res = fieldmapper.standardRequest(
198             ['open-ils.actor', 'open-ils.actor.verify_user_password'],
199             {params : [this.authtoken, barcode, null, hex_md5(passwd)]}
200         );
201
202         if(res == 0) {
203             // user-not-found results in login failure
204             this.handleAlert(
205                 dojo.string.substitute(localeStrings.LOGIN_FAILED, [barcode]),
206                 false, 'login-failure'
207             );
208             this.drawLoginPage();
209             return;
210         }
211     } 
212
213     // retrieve the fleshed user by barcode
214     this.patron = fieldmapper.standardRequest(
215         ['open-ils.actor', 'open-ils.actor.user.fleshed.retrieve_by_barcode'],
216         {params : [this.authtoken, barcode]}
217     );
218
219     var evt = openils.Event.parse(this.patron);
220     if(evt) {
221         this.handleAlert(
222             dojo.string.substitute(localeStrings.LOGIN_FAILED, [barcode]),
223             false, 'login-failure'
224         );
225         this.drawLoginPage();
226
227     } else {
228
229         this.handleAlert('', false, 'login-success');
230         dojo.byId('oils-selfck-user-banner').innerHTML = 'Welcome, ' + this.patron.first_given_name(); // TODO i18n
231         this.drawCircPage();
232     }
233 }
234
235
236 SelfCheckManager.prototype.handleAlert = function(message, shouldPopup, sound) {
237
238     console.log("Handling alert " + message);
239
240     dojo.byId('oils-selfck-status-div').innerHTML = message;
241
242     if(shouldPopup && this.orgSettings[SET_ALERT_POPUP]) 
243         alert(message);
244
245     if(sound && this.orgSettings[SET_ALERT_SOUND])
246         openils.Util.playAudioUrl(SelfCheckManager.audioConfig[sound]);
247 }
248
249
250 /**
251  * Manages the main input box
252  * @param msg The context message to display with the box
253  * @param clearOnly Don't update the context message, just clear the value and re-focus
254  * @param handler Optional "on-enter" handler.  
255  */
256 SelfCheckManager.prototype.updateScanBox = function(args) {
257     args = args || {};
258
259     if(args.select) {
260         selfckScanBox.domNode.select();
261     } else {
262         selfckScanBox.attr('value', '');
263     }
264
265     if(args.password) {
266         selfckScanBox.domNode.setAttribute('type', 'password');
267     } else {
268         selfckScanBox.domNode.setAttribute('type', '');
269     }
270
271     if(args.value)
272         selfckScanBox.attr('value', args.value);
273
274     if(args.msg) 
275         dojo.byId('oils-selfck-scan-text').innerHTML = args.msg;
276
277     if(selfckScanBox._lastHandler && (args.handler || args.clearHandler)) {
278         dojo.disconnect(selfckScanBox._lastHandler);
279     }
280
281     if(args.handler) {
282         selfckScanBox._lastHandler = dojo.connect(
283             selfckScanBox, 
284             'onKeyDown', 
285             function(e) {
286                 if(e.keyCode != dojo.keys.ENTER) 
287                     return;
288                 args.handler(selfckScanBox.attr('value'));
289             }
290         );
291     }
292
293     selfckScanBox.focus();
294 }
295
296 /**
297  *  Sets up the checkout/renewal interface
298  */
299 SelfCheckManager.prototype.drawCircPage = function() {
300
301     openils.Util.show('oils-selfck-circ-tbody');
302     this.goToTab('checkout');
303
304     while(this.itemsOutTbody.childNodes[0])
305         this.itemsOutTbody.removeChild(this.itemsOutTbody.childNodes[0]);
306
307     var self = this;
308     this.updateScanBox({
309         msg : 'Please enter an item barcode', // TODO i18n
310         handler : function(barcode) { self.checkout(barcode); }
311     });
312
313     if(!this.circTemplate)
314         this.circTemplate = this.circTbody.removeChild(dojo.byId('oils-selfck-circ-row'));
315
316     // items out, holds, and fines summaries
317
318     // fines summary
319     fieldmapper.standardRequest(
320         ['open-ils.actor', 'open-ils.actor.user.fines.summary'],
321         {   async : true,
322             params : [this.authtoken, this.patron.id()],
323             oncomplete : function(r) {
324                 var summary = openils.Util.readResponse(r);
325                 dojo.byId('oils-selfck-fines-total').innerHTML = 
326                     dojo.string.substitute(
327                         localeStrings.TOTAL_FINES_ACCOUNT, 
328                         [summary.balance_owed()]
329                     );
330             }
331         }
332     );
333
334     // holds summary
335     this.updateHoldsSummary();
336
337     // items out summary
338     this.updateCircSummary();
339
340     // render mock checkouts for debugging?
341     if(this.mockCheckouts) {
342         for(var i in [1,2,3]) 
343             this.displayCheckout(this.mockCheckout, 'checkout');
344     }
345 }
346
347
348 SelfCheckManager.prototype.drawItemsOutPage = function() {
349     openils.Util.hide('oils-selfck-circ-tbody');
350
351     this.goToTab('items_out');
352
353     while(this.itemsOutTbody.childNodes[0])
354         this.itemsOutTbody.removeChild(this.itemsOutTbody.childNodes[0]);
355
356     progressDialog.show(true);
357     
358     var self = this;
359     fieldmapper.standardRequest(
360         ['open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic'],
361         {
362             async : true,
363             params : [this.authtoken, this.patron.id()],
364             oncomplete : function(r) {
365
366                 var resp = openils.Util.readResponse(r);
367
368                 var circs = resp.sort(
369                     function(a, b) {
370                         if(a.circ.due_date() > b.circ.due_date())
371                             return -1;
372                         return 1;
373                     }
374                 );
375
376                 progressDialog.hide();
377
378                 self.itemsOut = [];
379                 dojo.forEach(circs,
380                     function(circ) {
381                         self.itemsOut.push(circ.circ.id());
382                         self.displayCheckout(
383                             {payload : circ}, 
384                             (circ.circ.parent_circ()) ? 'renew' : 'checkout',
385                             true
386                         );
387                     }
388                 );
389             }
390         }
391     );
392 }
393
394
395 SelfCheckManager.prototype.goToTab = function(name) {
396     this.tabName = name;
397
398     openils.Util.hide('oils-selfck-payment-page');
399     openils.Util.hide('oils-selfck-holds-page');
400     openils.Util.hide('oils-selfck-circ-page');
401     
402     switch(name) {
403         case 'checkout':
404             openils.Util.show('oils-selfck-circ-page');
405             break;
406         case 'items_out':
407             openils.Util.show('oils-selfck-circ-page');
408             break;
409         case 'holds':
410             openils.Util.show('oils-selfck-holds-page');
411             break;
412         case 'fines':
413             openils.Util.show('oils-selfck-payment-page');
414             break;
415     }
416 }
417
418
419 SelfCheckManager.prototype.printList = function() {
420     switch(this.tabName) {
421         case 'checkout':
422             this.printSessionReceipt();
423             break;
424         case 'items_out':
425             this.printItemsOutReceipt();
426             break;
427         case 'holds':
428             this.printHoldsReceipt();
429             break;
430         case 'fines':
431             this.printFinesReceipt();
432             break;
433     }
434 }
435
436 SelfCheckManager.prototype.updateHoldsSummary = function() {
437
438     if(!this.holdsSummary) {
439         var summary = fieldmapper.standardRequest(
440             ['open-ils.circ', 'open-ils.circ.holds.user_summary'],
441             {params : [this.authtoken, this.patron.id()]}
442         );
443
444         this.holdsSummary = {};
445         this.holdsSummary.ready = Number(summary['4']);
446         this.holdsSummary.total = 0;
447
448         for(var i in summary) 
449             this.holdsSummary.total += Number(summary[i]);
450     }
451
452     dojo.byId('oils-selfck-holds-total').innerHTML = 
453         dojo.string.substitute(
454             localeStrings.TOTAL_HOLDS, 
455             [this.holdsSummary.total]
456         );
457
458     dojo.byId('oils-selfck-holds-ready').innerHTML = 
459         dojo.string.substitute(
460             localeStrings.HOLDS_READY_FOR_PICKUP, 
461             [this.holdsSummary.ready]
462         );
463 }
464
465
466 SelfCheckManager.prototype.updateCircSummary = function(increment) {
467
468     if(!this.circSummary) {
469
470         var summary = fieldmapper.standardRequest(
471             ['open-ils.actor', 'open-ils.actor.user.checked_out.count'],
472             {params : [this.authtoken, this.patron.id()]}
473         );
474
475         this.circSummary = {
476             total : Number(summary.out) + Number(summary.overdue),
477             overdue : Number(summary.overdue),
478             session : 0
479         };
480     }
481
482     if(increment) {
483         // local checkout occurred.  Add to the total and the session.
484         this.circSummary.total += 1;
485         this.circSummary.session += 1;
486     }
487
488     dojo.byId('oils-selfck-circ-account-total').innerHTML = 
489         dojo.string.substitute(
490             localeStrings.TOTAL_ITEMS_ACCOUNT, 
491             [this.circSummary.total]
492         );
493
494     dojo.byId('oils-selfck-circ-session-total').innerHTML = 
495         dojo.string.substitute(
496             localeStrings.TOTAL_ITEMS_SESSION, 
497             [this.circSummary.session]
498         );
499 }
500
501
502 SelfCheckManager.prototype.drawHoldsPage = function() {
503
504     // TODO add option to hid scanBox
505     // this.updateScanBox(...)
506
507     this.goToTab('holds');
508
509     this.holdTbody = dojo.byId('oils-selfck-hold-tbody');
510     if(!this.holdTemplate)
511         this.holdTemplate = this.holdTbody.removeChild(dojo.byId('oils-selfck-hold-row'));
512     while(this.holdTbody.childNodes[0])
513         this.holdTbody.removeChild(this.holdTbody.childNodes[0]);
514
515     progressDialog.show(true);
516
517     var self = this;
518     fieldmapper.standardRequest( // fetch the hold IDs
519
520         ['open-ils.circ', 'open-ils.circ.holds.id_list.retrieve'],
521         {   async : true,
522             params : [this.authtoken, this.patron.id()],
523
524             oncomplete : function(r) { 
525                 var ids = openils.Util.readResponse(r);
526                 if(!ids || ids.length == 0) {
527                     progressDialog.hide();
528                     return;
529                 }
530
531                 fieldmapper.standardRequest( // fetch the hold objects with fleshed details
532                     ['open-ils.circ', 'open-ils.circ.hold.details.batch.retrieve.atomic'],
533                     {   async : true,
534                         params : [self.authtoken, ids],
535
536                         oncomplete : function(rr) {
537                             self.drawHolds(openils.Util.readResponse(rr));
538                         }
539                     }
540                 );
541             }
542         }
543     );
544 }
545
546 /**
547  * Fetch and add a single hold to the list of holds
548  */
549 SelfCheckManager.prototype.drawHolds = function(holds) {
550
551     holds = holds.sort(
552         // sort available holds to the top of the list
553         // followed by queue position order
554         function(a, b) {
555             if(a.status == 4) return -1;
556             if(a.queue_position < b.queue_position) return -1;
557             return 1;
558         }
559     );
560
561     this.holds = holds;
562
563     progressDialog.hide();
564
565     for(var i in holds) {
566
567         var data = holds[i];
568         var row = this.holdTemplate.cloneNode(true);
569
570         if(data.mvr.isbn()) {
571             this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + data.mvr.isbn());
572         }
573
574         this.byName(row, 'title').innerHTML = data.mvr.title();
575         this.byName(row, 'author').innerHTML = data.mvr.author();
576
577         if(data.status == 4) {
578
579             // hold is ready for pickup
580             this.byName(row, 'status').innerHTML = localeStrings.HOLD_STATUS_READY;
581
582         } else {
583
584             // hold is still pending
585             this.byName(row, 'status').innerHTML = 
586                 dojo.string.substitute(
587                     localeStrings.HOLD_STATUS_WAITING,
588                     [data.queue_position, data.potential_copies]
589                 );
590         }
591
592         this.holdTbody.appendChild(row);
593     }
594 }
595
596
597 SelfCheckManager.prototype.drawFinesPage = function() {
598
599     // TODO add option to hid scanBox
600     // this.updateScanBox(...)
601
602     this.goToTab('fines');
603     progressDialog.show(true);
604
605     this.finesTbody = dojo.byId('oils-selfck-fines-tbody');
606     if(!this.finesTemplate)
607         this.finesTemplate = this.finesTbody.removeChild(dojo.byId('oils-selfck-fines-row'));
608     while(this.finesTbody.childNodes[0])
609         this.finesTbody.removeChild(this.finesTbody.childNodes[0]);
610
611     var self = this;
612     var handler = function(dataList) {
613         self.finesCount = dataList.length;
614         for(var i in dataList) {
615             var data = dataList[i];
616             var row = self.finesTemplate.cloneNode(true);
617             var type = data.transaction.xact_type();
618             if(type == 'circulation') {
619                 self.byName(row, 'type').innerHTML = type;
620                 self.byName(row, 'details').innerHTML = data.record.title();
621             } else if(type == 'grocery') {
622                 self.byName(row, 'type').innerHTML = 'Miscellaneous'; // Go ahead and head off any confusion around "grocery".  TODO i18n
623                 self.byName(row, 'details').innerHTML = data.transaction.last_billing_type();
624             }
625             self.byName(row, 'total_owed').innerHTML = data.transaction.total_owed();
626             self.byName(row, 'total_paid').innerHTML = data.transaction.total_paid();
627             self.byName(row, 'balance').innerHTML = data.transaction.balance_owed();
628             self.finesTbody.appendChild(row);
629         }
630     }
631
632     fieldmapper.standardRequest( 
633         ['open-ils.actor', 'open-ils.actor.user.transactions.have_balance.fleshed'],
634         {   async : true,
635             params : [this.authtoken, this.patron.id()],
636             oncomplete : function(r) { 
637                 progressDialog.hide();
638                 handler(openils.Util.readResponse(r));
639             }
640         }
641     );
642 }
643
644
645 /**
646  * Check out a single item.  If the item is already checked 
647  * out to the patron, redirect to renew()
648  */
649 SelfCheckManager.prototype.checkout = function(barcode, override) {
650
651     this.prevCirc = null;
652
653     if(!barcode) {
654         this.updateScanbox(null, true);
655         return;
656     }
657
658     if(this.mockCheckouts) {
659         // if we're in mock-checkout mode, just insert another
660         // fake circ into the table and get out of here.
661         this.displayCheckout(this.mockCheckout, 'checkout');
662         return;
663     }
664
665     // TODO see if it's a patron barcode
666     // TODO see if this item has already been checked out in this session
667
668     var method = 'open-ils.circ.checkout.full';
669     if(override) method += '.override';
670
671     console.log("Checkout out item " + barcode + " with method " + method);
672
673     var result = fieldmapper.standardRequest(
674         ['open-ils.circ', 'open-ils.circ.checkout.full'],
675         {params: [
676             this.authtoken, {
677                 patron_id : this.patron.id(),
678                 copy_barcode : barcode
679             }
680         ]}
681     );
682
683     console.log(js2JSON(result));
684
685     var stat = this.handleXactResult('checkout', barcode, result);
686
687     if(stat.override) {
688         this.checkout(barcode, true);
689     } else if(stat.renew) {
690         this.renew(barcode);
691     }
692 }
693
694
695 SelfCheckManager.prototype.handleXactResult = function(action, item, result) {
696
697     var displayText = '';
698
699     // If true, the display message is important enough to pop up.  Whether or not
700     // an alert() actually occurs, depends on org unit settings
701     var popup = false;  
702
703     var sound = '';
704
705     // TODO handle lost/missing/etc checkin+checkout override steps
706     
707     var payload = result.payload || {};
708         
709     if(result.textcode == 'NO_SESSION') {
710
711         return this.logoutStaff();
712
713     } else if(result.textcode == 'SUCCESS') {
714
715         if(action == 'checkout') {
716
717             displayText = dojo.string.substitute(localeStrings.CHECKOUT_SUCCESS, [item]);
718             this.displayCheckout(result, 'checkout');
719
720             if(payload.holds_fulfilled && payload.holds_fulfilled.length) {
721                 // A hold was fulfilled, update the hold numbers in the circ summary
722                 console.log("fulfilled hold " + payload.holds_fulfilled + " during checkout");
723                 this.holdsSummary = null;
724                 this.updateHoldsSummary();
725             }
726
727             this.updateCircSummary(true);
728
729         } else if(action == 'renew') {
730
731             displayText = dojo.string.substitute(localeStrings.RENEW_SUCCESS, [item]);
732             this.displayCheckout(result, 'renew');
733         }
734
735         this.checkouts.push({circ : result.payload.circ.id()});
736         sound = 'checkout-success';
737         this.updateScanBox();
738
739     } else if(result.textcode == 'OPEN_CIRCULATION_EXISTS' && action == 'checkout') {
740
741         // Server says the item is already checked out.  If it's checked out to the
742         // current user, we may need to renew it.  
743
744         if(payload.old_circ) { 
745
746             /*
747             old_circ refers to the previous checkout IFF it's for the same user. 
748             If no auto-renew interval is not defined, assume we should renew it
749             If an auto-renew interval is defined and the payload comes back with
750             auto_renew set to true, do the renewal.  Otherwise, let the patron know
751             the item is already checked out to them.  */
752
753             if( !this.orgSettings[SET_AUTO_RENEW_INTERVAL] ||
754                 (this.orgSettings[SET_AUTO_RENEW_INTERVAL] && payload.auto_renew) ) {
755                 this.prevCirc = payload.old_circ.id();
756                 return { renew : true };
757             }
758
759             popup = true;
760             sound = 'checkout-failure';
761             displayText = dojo.string.substitute(localeStrings.ALREADY_OUT, [item]);
762
763         } else {
764             
765             // item is checked out to some other user
766             popup = true;
767             sound = 'checkout-failure';
768             displayText = dojo.string.substitute(localeStrings.OPEN_CIRCULATION_EXISTS, [item]);
769         }
770
771         this.updateScanBox({select:true});
772
773     } else {
774
775         var overrideEvents = this.orgSettings[SET_AUTO_OVERRIDE_EVENTS];
776     
777         if(overrideEvents && overrideEvents.length) {
778             
779             // see if the events we received are all in the list of
780             // events to override
781     
782             if(!result.length) result = [result];
783     
784             var override = true;
785             for(var i = 0; i < result.length; i++) {
786                 var match = overrideEvents.filter(
787                     function(e) { return (e == result[i].textcode); })[0];
788                 if(!match) {
789                     override = false;
790                     break;
791                 }
792             }
793
794             if(override) 
795                 return { override : true };
796         }
797     
798         this.updateScanBox({select : true});
799         popup = true;
800         sound = 'checkout-failure';
801
802         if(action == 'renew')
803             this.checkouts.push({circ : this.prevCirc, renewal_failure : true});
804
805         if(result.length) 
806             result = result[0];
807
808         switch(result.textcode) {
809
810             // TODO custom handler for blocking penalties
811
812             case 'MAX_RENEWALS_REACHED' :
813                 displayText = dojo.string.substitute(
814                     localeStrings.MAX_RENEWALS, [item]);
815                 break;
816
817             case 'ITEM_NOT_CATALOGED' :
818                 displayText = dojo.string.substitute(
819                     localeStrings.ITEM_NOT_CATALOGED, [item]);
820                 break;
821
822             case 'OPEN_CIRCULATION_EXISTS' :
823                 displayText = dojo.string.substitute(
824                     localeStrings.OPEN_CIRCULATION_EXISTS, [item]);
825                 break;
826
827             default:
828                 console.error('Unhandled event ' + result.textcode);
829
830                 if(action == 'checkout' || action == 'renew') {
831                     displayText = dojo.string.substitute(
832                         localeStrings.GENERIC_CIRC_FAILURE, [item]);
833                 } else {
834                     displayText = dojo.string.substitute(
835                         localeStrings.UNKNOWN_ERROR, [result.textcode]);
836                 }
837         }
838     }
839
840     this.handleAlert(displayText, popup, sound);
841     return {};
842 }
843
844
845 /**
846  * Renew an item
847  */
848 SelfCheckManager.prototype.renew = function(barcode, override) {
849
850     var method = 'open-ils.circ.renew';
851     if(override) method += '.override';
852
853     console.log("Renewing item " + barcode + " with method " + method);
854
855     var result = fieldmapper.standardRequest(
856         ['open-ils.circ', method],
857         {params: [
858             this.authtoken, {
859                 patron_id : this.patron.id(),
860                 copy_barcode : barcode
861             }
862         ]}
863     );
864
865     console.log(js2JSON(result));
866
867     var stat = this.handleXactResult('renew', barcode, result);
868
869     if(stat.override)
870         this.renew(barcode, true);
871 }
872
873 /**
874  * Display the result of a checkout or renewal in the items out table
875  */
876 SelfCheckManager.prototype.displayCheckout = function(evt, type, itemsOut) {
877
878     var copy = evt.payload.copy;
879     var record = evt.payload.record;
880     var circ = evt.payload.circ;
881     var row = this.circTemplate.cloneNode(true);
882
883     if(record.isbn()) {
884         this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + record.isbn());
885     }
886
887     this.byName(row, 'barcode').innerHTML = copy.barcode();
888     this.byName(row, 'title').innerHTML = record.title();
889     this.byName(row, 'author').innerHTML = record.author();
890     this.byName(row, 'remaining').innerHTML = circ.renewal_remaining();
891     openils.Util.show(this.byName(row, type));
892
893     var date = dojo.date.stamp.fromISOString(circ.due_date());
894     this.byName(row, 'due_date').innerHTML = 
895         dojo.date.locale.format(date, {selector : 'date'});
896
897     // put new circs at the top of the list
898     var tbody = this.circTbody;
899     if(itemsOut) tbody = this.itemsOutTbody;
900     tbody.insertBefore(row, tbody.getElementsByTagName('tr')[0]);
901 }
902
903
904 SelfCheckManager.prototype.byName = function(node, name) {
905     return dojo.query('[name=' + name+']', node)[0];
906 }
907
908
909 SelfCheckManager.prototype.initPrinter = function() {
910     try { // Mozilla only
911                 netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
912         netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
913         netscape.security.PrivilegeManager.enablePrivilege('UniversalPreferencesRead');
914         netscape.security.PrivilegeManager.enablePrivilege('UniversalPreferencesWrite');
915         var pref = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
916         if (pref)
917             pref.setBoolPref('print.always_print_silent', true);
918     } catch(E) {
919         console.log("Unable to initialize auto-printing"); 
920     }
921 }
922
923 /**
924  * Print a receipt for this session's checkouts
925  */
926 SelfCheckManager.prototype.printSessionReceipt = function(callback) {
927
928     var circIds = [];
929     var circCtx = []; // circ context data.  in this case, renewal_failure info
930
931     // collect the circs and failure info
932     dojo.forEach(
933         this.checkouts, 
934         function(blob) {
935             circIds.push(blob.circ);
936             circCtx.push({renewal_failure:blob.renewal_failure});
937         }
938     );
939
940     var params = [
941         this.authtoken, 
942         this.staff.ws_ou(),
943         null,
944         'format.selfcheck.checkout',
945         'print-on-demand',
946         circIds,
947         circCtx
948     ];
949
950     var self = this;
951     fieldmapper.standardRequest(
952         ['open-ils.circ', 'open-ils.circ.fire_circ_trigger_events'],
953         {   
954             async : true,
955             params : params,
956             oncomplete : function(r) {
957                 var resp = openils.Util.readResponse(r);
958                 var output = resp.template_output();
959                 if(output) {
960                     self.printData(output.data(), self.checkouts.length, callback); 
961                 } else {
962                     var error = resp.error_output();
963                     if(error) {
964                         throw new Error("Error creating receipt: " + error.data());
965                     } else {
966                         throw new Error("No receipt data returned from server");
967                     }
968                 }
969             }
970         }
971     );
972 }
973
974 SelfCheckManager.prototype.printData = function(data, numItems, callback) {
975
976     var win = window.open('', '', 'resizable,width=700,height=500,scrollbars=1'); 
977     win.document.body.innerHTML = data;
978     win.print();
979
980     /*
981      * There is no way to know when the browser is done printing.
982      * Make a best guess at when to close the print window by basing
983      * the setTimeout wait on the number of items to be printed plus
984      * a small buffer
985      */
986     var sleepTime = 1000;
987     if(numItems > 0) 
988         sleepTime += (numItems / 2) * 1000;
989
990     setTimeout(
991         function() { 
992             win.close(); // close the print window
993             if(callback)
994                 callback(); // fire optional post-print callback
995         },
996         sleepTime 
997     );
998 }
999
1000
1001 /**
1002  * Print a receipt for this user's items out
1003  */
1004 SelfCheckManager.prototype.printItemsOutReceipt = function(callback) {
1005
1006     if(!this.itemsOut.length) return;
1007
1008     progressDialog.show(true);
1009
1010     var params = [
1011         this.authtoken, 
1012         this.staff.ws_ou(),
1013         null,
1014         'format.selfcheck.items_out',
1015         'print-on-demand',
1016         this.itemsOut
1017     ];
1018
1019     var self = this;
1020     fieldmapper.standardRequest(
1021         ['open-ils.circ', 'open-ils.circ.fire_circ_trigger_events'],
1022         {   
1023             async : true,
1024             params : params,
1025             oncomplete : function(r) {
1026                 progressDialog.hide();
1027                 var resp = openils.Util.readResponse(r);
1028                 var output = resp.template_output();
1029                 if(output) {
1030                     self.printData(output.data(), self.itemsOut.length, callback); 
1031                 } else {
1032                     var error = resp.error_output();
1033                     if(error) {
1034                         throw new Error("Error creating receipt: " + error.data());
1035                     } else {
1036                         throw new Error("No receipt data returned from server");
1037                     }
1038                 }
1039             }
1040         }
1041     );
1042 }
1043
1044 /**
1045  * Print a receipt for this user's items out
1046  */
1047 SelfCheckManager.prototype.printHoldsReceipt = function(callback) {
1048
1049     if(!this.holds.length) return;
1050
1051     progressDialog.show(true);
1052
1053     var holdIds = [];
1054     var holdData = [];
1055
1056     dojo.forEach(this.holds,
1057         function(data) {
1058             holdIds.push(data.hold.id());
1059             if(data.status == 4) {
1060                 holdData.push({ready : true});
1061             } else {
1062                 holdData.push({
1063                     queue_position : data.queue_position, 
1064                     potential_copies : data.potential_copies
1065                 });
1066             }
1067         }
1068     );
1069
1070     var params = [
1071         this.authtoken, 
1072         this.staff.ws_ou(),
1073         null,
1074         'format.selfcheck.holds',
1075         'print-on-demand',
1076         holdIds,
1077         holdData
1078     ];
1079
1080     var self = this;
1081     fieldmapper.standardRequest(
1082         ['open-ils.circ', 'open-ils.circ.fire_hold_trigger_events'],
1083         {   
1084             async : true,
1085             params : params,
1086             oncomplete : function(r) {
1087                 progressDialog.hide();
1088                 var resp = openils.Util.readResponse(r);
1089                 var output = resp.template_output();
1090                 if(output) {
1091                     self.printData(output.data(), self.holds.length, callback); 
1092                 } else {
1093                     var error = resp.error_output();
1094                     if(error) {
1095                         throw new Error("Error creating receipt: " + error.data());
1096                     } else {
1097                         throw new Error("No receipt data returned from server");
1098                     }
1099                 }
1100             }
1101         }
1102     );
1103 }
1104
1105
1106 /**
1107  * Print a receipt for this user's items out
1108  */
1109 SelfCheckManager.prototype.printFinesReceipt = function(callback) {
1110
1111     progressDialog.show(true);
1112
1113     var params = [
1114         this.authtoken, 
1115         this.staff.ws_ou(),
1116         null,
1117         'format.selfcheck.fines',
1118         'print-on-demand',
1119         [this.patron.id()]
1120     ];
1121
1122     var self = this;
1123     fieldmapper.standardRequest(
1124         ['open-ils.circ', 'open-ils.circ.fire_user_trigger_events'],
1125         {   
1126             async : true,
1127             params : params,
1128             oncomplete : function(r) {
1129                 progressDialog.hide();
1130                 var resp = openils.Util.readResponse(r);
1131                 var output = resp.template_output();
1132                 if(output) {
1133                     self.printData(output.data(), self.finesCount, callback); 
1134                 } else {
1135                     var error = resp.error_output();
1136                     if(error) {
1137                         throw new Error("Error creating receipt: " + error.data());
1138                     } else {
1139                         throw new Error("No receipt data returned from server");
1140                     }
1141                 }
1142             }
1143         }
1144     );
1145 }
1146
1147
1148
1149
1150 /**
1151  * Logout the patron and return to the login page
1152  */
1153 SelfCheckManager.prototype.logoutPatron = function(print) {
1154     if(print && this.checkouts.length) {
1155         this.printSessionReceipt(
1156             function() {
1157                 location.href = location.href;
1158             }
1159         );
1160     } else {
1161         location.href = location.href;
1162     }
1163 }
1164
1165
1166 /**
1167  * Fire up the manager on page load
1168  */
1169 openils.Util.addOnLoad(
1170     function() {
1171         new SelfCheckManager().init();
1172     }
1173 );