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