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