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