]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/circ/selfcheck/selfcheck.js
display first name instead of usrname in self-check display as a security/obscurity...
[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
888 SelfCheckManager.prototype.handleXactResult = function(action, item, result) {
889
890     var displayText = '';
891
892     // If true, the display message is important enough to pop up.  Whether or not
893     // an alert() actually occurs, depends on org unit settings
894     var popup = false;  
895     var sound = ''; // sound file reference
896     var payload = result.payload || {};
897     var overrideEvents = this.orgSettings[SET_AUTO_OVERRIDE_EVENTS];
898         
899     if(result.textcode == 'NO_SESSION') {
900
901         return this.logoutStaff();
902
903     } else if(result.textcode == 'SUCCESS') {
904
905         if(action == 'checkout') {
906
907             displayText = dojo.string.substitute(localeStrings.CHECKOUT_SUCCESS, [item]);
908             this.displayCheckout(result, 'checkout');
909
910             if(payload.holds_fulfilled && payload.holds_fulfilled.length) {
911                 // A hold was fulfilled, update the hold numbers in the circ summary
912                 console.log("fulfilled hold " + payload.holds_fulfilled + " during checkout");
913                 this.holdsSummary = null;
914                 this.updateHoldsSummary();
915             }
916
917             this.updateCircSummary(true);
918
919         } else if(action == 'renew') {
920
921             displayText = dojo.string.substitute(localeStrings.RENEW_SUCCESS, [item]);
922             this.displayCheckout(result, 'renew');
923         }
924
925         this.checkouts.push({circ : result.payload.circ.id()});
926         sound = 'checkout-success';
927         this.updateScanBox();
928
929     } else if(result.textcode == 'OPEN_CIRCULATION_EXISTS' && action == 'checkout') {
930
931         // Server says the item is already checked out.  If it's checked out to the
932         // current user, we may need to renew it.  
933
934         if(payload.old_circ) { 
935
936             /*
937             old_circ refers to the previous checkout IFF it's for the same user. 
938             If no auto-renew interval is not defined, assume we should renew it
939             If an auto-renew interval is defined and the payload comes back with
940             auto_renew set to true, do the renewal.  Otherwise, let the patron know
941             the item is already checked out to them.  */
942
943             if( !this.orgSettings[SET_AUTO_RENEW_INTERVAL] ||
944                 (this.orgSettings[SET_AUTO_RENEW_INTERVAL] && payload.auto_renew) ) {
945                 this.prevCirc = payload.old_circ.id();
946                 return { renew : true };
947             }
948
949             popup = true;
950             sound = 'checkout-failure';
951             displayText = dojo.string.substitute(localeStrings.ALREADY_OUT, [item]);
952
953         } else {
954
955             if( // copy is marked lost.  if configured to do so, check it in and try again.
956                 result.payload.copy && 
957                 result.payload.copy.status() == /* LOST */ 3 &&
958                 overrideEvents && overrideEvents.length &&
959                 overrideEvents.indexOf('COPY_STATUS_LOST') != -1) {
960
961                     if(this.checkin(item)) {
962                         return { doOver : true };
963                     }
964             }
965
966             
967             // item is checked out to some other user
968             popup = true;
969             sound = 'checkout-failure';
970             displayText = dojo.string.substitute(localeStrings.OPEN_CIRCULATION_EXISTS, [item]);
971         }
972
973         this.updateScanBox({select:true});
974
975     } else {
976
977     
978         if(overrideEvents && overrideEvents.length) {
979             
980             // see if the events we received are all in the list of
981             // events to override
982     
983             if(!result.length) result = [result];
984     
985             var override = true;
986             for(var i = 0; i < result.length; i++) {
987                 var match = overrideEvents.filter(
988                     function(e) { return (e == result[i].textcode); })[0];
989                 if(!match) {
990                     override = false;
991                     break;
992                 }
993
994                 if(result[i].textcode == 'COPY_IN_TRANSIT') {
995                     // to override a transit, we have to abort the transit and check it in first
996                     if(this.checkin(item, true)) {
997                         return { doOver : true };
998                     } else {
999                         override = false;
1000                     }
1001
1002                 }
1003             }
1004
1005             if(override) 
1006                 return { override : true };
1007         }
1008     
1009         this.updateScanBox({select : true});
1010         popup = true;
1011         sound = 'checkout-failure';
1012
1013         if(action == 'renew')
1014             this.checkouts.push({circ : this.prevCirc, renewal_failure : true});
1015
1016         if(result.length) 
1017             result = result[0];
1018
1019         switch(result.textcode) {
1020
1021             // TODO custom handler for blocking penalties
1022
1023             case 'MAX_RENEWALS_REACHED' :
1024                 displayText = dojo.string.substitute(
1025                     localeStrings.MAX_RENEWALS, [item]);
1026                 break;
1027
1028             case 'ITEM_NOT_CATALOGED' :
1029                 displayText = dojo.string.substitute(
1030                     localeStrings.ITEM_NOT_CATALOGED, [item]);
1031                 break;
1032
1033             case 'OPEN_CIRCULATION_EXISTS' :
1034                 displayText = dojo.string.substitute(
1035                     localeStrings.OPEN_CIRCULATION_EXISTS, [item]);
1036
1037                 break;
1038
1039             default:
1040                 console.error('Unhandled event ' + result.textcode);
1041
1042                 if(action == 'checkout' || action == 'renew') {
1043                     displayText = dojo.string.substitute(
1044                         localeStrings.GENERIC_CIRC_FAILURE, [item]);
1045                 } else {
1046                     displayText = dojo.string.substitute(
1047                         localeStrings.UNKNOWN_ERROR, [result.textcode]);
1048                 }
1049         }
1050     }
1051
1052     this.handleAlert(displayText, popup, sound);
1053     return {};
1054 }
1055
1056
1057 /**
1058  * Renew an item
1059  */
1060 SelfCheckManager.prototype.renew = function(barcode, override) {
1061
1062     var method = 'open-ils.circ.renew';
1063     if(override) method += '.override';
1064
1065     console.log("Renewing item " + barcode + " with method " + method);
1066
1067     var result = fieldmapper.standardRequest(
1068         ['open-ils.circ', method],
1069         {params: [
1070             this.authtoken, {
1071                 patron_id : this.patron.id(),
1072                 copy_barcode : barcode
1073             }
1074         ]}
1075     );
1076
1077     console.log(js2JSON(result));
1078
1079     var stat = this.handleXactResult('renew', barcode, result);
1080
1081     if(stat.override)
1082         this.renew(barcode, true);
1083 }
1084
1085 /**
1086  * Display the result of a checkout or renewal in the items out table
1087  */
1088 SelfCheckManager.prototype.displayCheckout = function(evt, type, itemsOut) {
1089
1090     var copy = evt.payload.copy;
1091     var record = evt.payload.record;
1092     var circ = evt.payload.circ;
1093     var row = this.circTemplate.cloneNode(true);
1094
1095     if(record.isbn()) {
1096         this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + record.isbn());
1097     }
1098
1099     this.byName(row, 'barcode').innerHTML = copy.barcode();
1100     this.byName(row, 'title').innerHTML = record.title();
1101     this.byName(row, 'author').innerHTML = record.author();
1102     this.byName(row, 'remaining').innerHTML = circ.renewal_remaining();
1103     openils.Util.show(this.byName(row, type));
1104
1105     var date = dojo.date.stamp.fromISOString(circ.due_date());
1106     this.byName(row, 'due_date').innerHTML = 
1107         dojo.date.locale.format(date, {selector : 'date'});
1108
1109     // put new circs at the top of the list
1110     var tbody = this.circTbody;
1111     if(itemsOut) tbody = this.itemsOutTbody;
1112     tbody.insertBefore(row, tbody.getElementsByTagName('tr')[0]);
1113 }
1114
1115
1116 SelfCheckManager.prototype.byName = function(node, name) {
1117     return dojo.query('[name=' + name+']', node)[0];
1118 }
1119
1120
1121 SelfCheckManager.prototype.initPrinter = function() {
1122     try { // Mozilla only
1123                 netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
1124         netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
1125         netscape.security.PrivilegeManager.enablePrivilege('UniversalPreferencesRead');
1126         netscape.security.PrivilegeManager.enablePrivilege('UniversalPreferencesWrite');
1127         var pref = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
1128         if (pref)
1129             pref.setBoolPref('print.always_print_silent', true);
1130     } catch(E) {
1131         console.log("Unable to initialize auto-printing"); 
1132     }
1133 }
1134
1135 /**
1136  * Print a receipt for this session's checkouts
1137  */
1138 SelfCheckManager.prototype.printSessionReceipt = function(callback) {
1139
1140     var circIds = [];
1141     var circCtx = []; // circ context data.  in this case, renewal_failure info
1142
1143     // collect the circs and failure info
1144     dojo.forEach(
1145         this.checkouts, 
1146         function(blob) {
1147             circIds.push(blob.circ);
1148             circCtx.push({renewal_failure:blob.renewal_failure});
1149         }
1150     );
1151
1152     var params = [
1153         this.authtoken, 
1154         this.staff.ws_ou(),
1155         null,
1156         'format.selfcheck.checkout',
1157         'print-on-demand',
1158         circIds,
1159         circCtx
1160     ];
1161
1162     var self = this;
1163     fieldmapper.standardRequest(
1164         ['open-ils.circ', 'open-ils.circ.fire_circ_trigger_events'],
1165         {   
1166             async : true,
1167             params : params,
1168             oncomplete : function(r) {
1169                 var resp = openils.Util.readResponse(r);
1170                 var output = resp.template_output();
1171                 if(output) {
1172                     self.printData(output.data(), self.checkouts.length, callback); 
1173                 } else {
1174                     var error = resp.error_output();
1175                     if(error) {
1176                         throw new Error("Error creating receipt: " + error.data());
1177                     } else {
1178                         throw new Error("No receipt data returned from server");
1179                     }
1180                 }
1181             }
1182         }
1183     );
1184 }
1185
1186 SelfCheckManager.prototype.printData = function(data, numItems, callback) {
1187
1188     var win = window.open('', '', 'resizable,width=700,height=500,scrollbars=1'); 
1189     win.document.body.innerHTML = data;
1190     win.print();
1191
1192     /*
1193      * There is no way to know when the browser is done printing.
1194      * Make a best guess at when to close the print window by basing
1195      * the setTimeout wait on the number of items to be printed plus
1196      * a small buffer
1197      */
1198     var sleepTime = 1000;
1199     if(numItems > 0) 
1200         sleepTime += (numItems / 2) * 1000;
1201
1202     setTimeout(
1203         function() { 
1204             win.close(); // close the print window
1205             if(callback)
1206                 callback(); // fire optional post-print callback
1207         },
1208         sleepTime 
1209     );
1210 }
1211
1212
1213 /**
1214  * Print a receipt for this user's items out
1215  */
1216 SelfCheckManager.prototype.printItemsOutReceipt = function(callback) {
1217
1218     if(!this.itemsOut.length) return;
1219
1220     progressDialog.show(true);
1221
1222     var params = [
1223         this.authtoken, 
1224         this.staff.ws_ou(),
1225         null,
1226         'format.selfcheck.items_out',
1227         'print-on-demand',
1228         this.itemsOut
1229     ];
1230
1231     var self = this;
1232     fieldmapper.standardRequest(
1233         ['open-ils.circ', 'open-ils.circ.fire_circ_trigger_events'],
1234         {   
1235             async : true,
1236             params : params,
1237             oncomplete : function(r) {
1238                 progressDialog.hide();
1239                 var resp = openils.Util.readResponse(r);
1240                 var output = resp.template_output();
1241                 if(output) {
1242                     self.printData(output.data(), self.itemsOut.length, callback); 
1243                 } else {
1244                     var error = resp.error_output();
1245                     if(error) {
1246                         throw new Error("Error creating receipt: " + error.data());
1247                     } else {
1248                         throw new Error("No receipt data returned from server");
1249                     }
1250                 }
1251             }
1252         }
1253     );
1254 }
1255
1256 /**
1257  * Print a receipt for this user's items out
1258  */
1259 SelfCheckManager.prototype.printHoldsReceipt = function(callback) {
1260
1261     if(!this.holds.length) return;
1262
1263     progressDialog.show(true);
1264
1265     var holdIds = [];
1266     var holdData = [];
1267
1268     dojo.forEach(this.holds,
1269         function(data) {
1270             holdIds.push(data.hold.id());
1271             if(data.status == 4) {
1272                 holdData.push({ready : true});
1273             } else {
1274                 holdData.push({
1275                     queue_position : data.queue_position, 
1276                     potential_copies : data.potential_copies
1277                 });
1278             }
1279         }
1280     );
1281
1282     var params = [
1283         this.authtoken, 
1284         this.staff.ws_ou(),
1285         null,
1286         'format.selfcheck.holds',
1287         'print-on-demand',
1288         holdIds,
1289         holdData
1290     ];
1291
1292     var self = this;
1293     fieldmapper.standardRequest(
1294         ['open-ils.circ', 'open-ils.circ.fire_hold_trigger_events'],
1295         {   
1296             async : true,
1297             params : params,
1298             oncomplete : function(r) {
1299                 progressDialog.hide();
1300                 var resp = openils.Util.readResponse(r);
1301                 var output = resp.template_output();
1302                 if(output) {
1303                     self.printData(output.data(), self.holds.length, callback); 
1304                 } else {
1305                     var error = resp.error_output();
1306                     if(error) {
1307                         throw new Error("Error creating receipt: " + error.data());
1308                     } else {
1309                         throw new Error("No receipt data returned from server");
1310                     }
1311                 }
1312             }
1313         }
1314     );
1315 }
1316
1317
1318 /**
1319  * Print a receipt for this user's items out
1320  */
1321 SelfCheckManager.prototype.printFinesReceipt = function(callback) {
1322
1323     progressDialog.show(true);
1324
1325     var params = [
1326         this.authtoken, 
1327         this.staff.ws_ou(),
1328         null,
1329         'format.selfcheck.fines',
1330         'print-on-demand',
1331         [this.patron.id()]
1332     ];
1333
1334     var self = this;
1335     fieldmapper.standardRequest(
1336         ['open-ils.circ', 'open-ils.circ.fire_user_trigger_events'],
1337         {   
1338             async : true,
1339             params : params,
1340             oncomplete : function(r) {
1341                 progressDialog.hide();
1342                 var resp = openils.Util.readResponse(r);
1343                 var output = resp.template_output();
1344                 if(output) {
1345                     self.printData(output.data(), self.finesCount, callback); 
1346                 } else {
1347                     var error = resp.error_output();
1348                     if(error) {
1349                         throw new Error("Error creating receipt: " + error.data());
1350                     } else {
1351                         throw new Error("No receipt data returned from server");
1352                     }
1353                 }
1354             }
1355         }
1356     );
1357 }
1358
1359
1360
1361
1362 /**
1363  * Logout the patron and return to the login page
1364  */
1365 SelfCheckManager.prototype.logoutPatron = function(print) {
1366     if(print && this.checkouts.length) {
1367         this.printSessionReceipt(
1368             function() {
1369                 location.href = location.href;
1370             }
1371         );
1372     } else {
1373         location.href = location.href;
1374     }
1375 }
1376
1377
1378 /**
1379  * Fire up the manager on page load
1380  */
1381 openils.Util.addOnLoad(
1382     function() {
1383         new SelfCheckManager().init();
1384     }
1385 );