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