plugged in autorenewal, more event handling
[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
9 dojo.requireLocalization('openils.circ', 'selfcheck');
10 var localeStrings = dojo.i18n.getLocalization('openils.circ', 'selfcheck');
11
12
13 const SET_BARCODE_REGEX = 'opac.barcode_regex';
14 const SET_PATRON_TIMEOUT = 'circ.selfcheck.patron_login_timeout';
15 const SET_ALERT_ON_CHECKOUT_EVENT = 'circ.selfcheck.alert_on_checkout_event';
16 const SET_AUTO_OVERRIDE_EVENTS = 'circ.selfcheck.auto_override_checkout_events';
17 const SET_PATRON_PASSWORD_REQUIRED = 'circ.selfcheck.patron_password_required';
18
19 //openils.Util.playAudioUrl('/xul/server/skin/media/audio/bonus.wav');
20
21 function SelfCheckManager() {
22
23     this.cgi = new openils.CGI();
24     this.staff = null; 
25     this.workstation = null;
26     this.authtoken = null;
27
28     this.patron = null; 
29     this.patronBarcodeRegex = null;
30
31     // current item barcode
32     this.itemBarcode = null; 
33
34     // are we currently performing a renewal?
35     this.isRenewal = false; 
36
37     // dict of org unit settings for "here"
38     this.orgSettings = {};
39
40     // Construct a mock checkout for debugging purposes
41     if(this.mockCheckouts = this.cgi.param('mock-circ')) {
42
43         this.mockCheckout = {
44             payload : {
45                 record : new fieldmapper.mvr(),
46                 copy : new fieldmapper.acp(),
47                 circ : new fieldmapper.circ()
48             }
49         };
50
51         this.mockCheckout.payload.record.title('Jazz improvisation for guitar');
52         this.mockCheckout.payload.record.author('Wise, Les');
53         this.mockCheckout.payload.record.isbn('0634033565');
54         this.mockCheckout.payload.copy.barcode('123456789');
55         this.mockCheckout.payload.circ.renewal_remaining(1);
56         this.mockCheckout.payload.circ.parent_circ(1);
57         this.mockCheckout.payload.circ.due_date('2012-12-21');
58     }
59 }
60
61
62
63 /**
64  * Fetch the org-unit settings, initialize the display, etc.
65  */
66 SelfCheckManager.prototype.init = function() {
67
68     this.staff = openils.User.user;
69     this.workstation = openils.User.workstation;
70     this.authtoken = openils.User.authtoken;
71     this.loadOrgSettings();
72
73     
74     var self = this;
75     // connect onclick handlers to the various navigation links
76     var linkHandlers = {
77         'oils-selfck-hold-details-link' : function() { self.drawHoldsPage(); },
78         'oils-selfck-nav-holds' : function() { self.drawHoldsPage(); },
79         'oils-selfck-pay-fines-link' : function() { self.drawFinesPage(); },
80         'oils-selfck-nav-fines' : function() { self.drawFinesPage(); },
81         'oils-selfck-nav-home' : function() { self.drawCircPage(); },
82         'oils-selfck-nav-logout' : function() { self.logoutPatron(); }
83     }
84
85     for(var id in linkHandlers) 
86         dojo.connect(dojo.byId(id), 'onclick', linkHandlers[id]);
87
88
89     if(this.cgi.param('patron')) {
90         
91         // Patron barcode via cgi param.  Mainly used for debugging and
92         // only works if password is not required by policy
93         this.loginPatron(this.cgi.param('patron'));
94
95     } else {
96         this.drawLoginPage();
97     }
98 }
99
100 /**
101  * Loads the org unit settings
102  */
103 SelfCheckManager.prototype.loadOrgSettings = function() {
104
105     var settings = fieldmapper.aou.fetchOrgSettingBatch(
106         this.staff.ws_ou(), [
107             SET_BARCODE_REGEX,
108             SET_PATRON_TIMEOUT,
109             SET_ALERT_ON_CHECKOUT_EVENT,
110             SET_AUTO_OVERRIDE_EVENTS,
111         ]
112     );
113
114     for(k in settings) {
115         if(settings[k])
116             this.orgSettings[k] = settings[k].value;
117     }
118
119     if(settings[SET_BARCODE_REGEX]) 
120         this.patronBarcodeRegex = new RegExp(settings[SET_BARCODE_REGEX].value);
121 }
122
123 SelfCheckManager.prototype.drawLoginPage = function() {
124     var self = this;
125
126     var bcHandler = function(barcode) {
127         // handle patron barcode entry
128
129         if(self.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) {
130             
131             // password is required.  wire up the scan box to read it
132             self.updateScanBox({
133                 msg : 'Please enter your password', // TODO i18n 
134                 handler : function(pw) { self.loginPatron(barcode, pw); }
135             });
136
137         } else {
138             // password is not required, go ahead and login
139             self.loginPatron(barcode);
140         }
141     };
142
143     this.updateScanBox({
144         msg : 'Please log in with your library barcode.', // TODO
145         handler : bcHandler
146     });
147 }
148
149 /**
150  * Login the patron.  
151  */
152 SelfCheckManager.prototype.loginPatron = function(barcode, passwd) {
153
154     if(this.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) {
155
156         // patron password is required.  Verify it.
157
158         var res = fieldmapper.standardRequest(
159             ['open-ils.actor', 'open-ils.actor.verify_user_password'],
160             {params : [this.authtoken, barcode, null, hex_md5(passwd)]}
161         );
162
163         if(res == 0) {
164             // user-not-found results in login failure
165             this.handleXactResult('login', barcode, {textcode : 'ACTOR_USER_NOT_FOUND'});
166         }
167     } 
168
169     // retrieve the fleshed user by barcode
170     this.patron = fieldmapper.standardRequest(
171         ['open-ils.actor', 'open-ils.actor.user.fleshed.retrieve_by_barcode'],
172         {params : [this.authtoken, barcode]}
173     );
174
175     var evt = openils.Event.parse(this.patron);
176     if(evt) {
177         this.handleXactResult('login', barcode, evt);
178
179     } else {
180
181         dojo.byId('oils-selfck-status-div').innerHTML = '';
182         dojo.byId('oils-selfck-user-banner').innerHTML = 'Welcome, ' + this.patron.usrname(); // TODO i18n
183         this.drawCircPage();
184     }
185 }
186
187
188 /**
189  * Manages the main input box
190  * @param msg The context message to display with the box
191  * @param clearOnly Don't update the context message, just clear the value and re-focus
192  * @param handler Optional "on-enter" handler.  
193  */
194 SelfCheckManager.prototype.updateScanBox = function(args) {
195     args = args || {};
196
197     if(args.select) {
198         selfckScanBox.domNode.select();
199     } else {
200         selfckScanBox.attr('value', '');
201     }
202
203     if(args.value)
204         selfckScanBox.attr('value', args.value);
205
206     if(args.msg) 
207         dojo.byId('oils-selfck-scan-text').innerHTML = args.msg;
208
209     if(selfckScanBox._lastHandler && (args.handler || args.clearHandler)) {
210         dojo.disconnect(selfckScanBox._lastHandler);
211     }
212
213     if(args.handler) {
214         selfckScanBox._lastHandler = dojo.connect(
215             selfckScanBox, 
216             'onKeyDown', 
217             function(e) {
218                 if(e.keyCode != dojo.keys.ENTER) 
219                     return;
220                 args.handler(selfckScanBox.attr('value'));
221             }
222         );
223     }
224
225     selfckScanBox.focus();
226 }
227
228 /**
229  *  Sets up the checkout/renewal interface
230  */
231 SelfCheckManager.prototype.drawCircPage = function() {
232
233     var self = this;
234     this.updateScanBox({
235         msg : 'Please enter an item barcode', // TODO i18n
236         handler : function(barcode) { self.checkout(barcode); }
237     });
238
239     openils.Util.hide('oils-selfck-payment-page');
240     openils.Util.hide('oils-selfck-holds-page');
241     openils.Util.show('oils-selfck-circ-page');
242
243     this.circTbody = dojo.byId('oils-selfck-circ-tbody');
244     if(!this.circTemplate)
245         this.circTemplate = this.circTbody.removeChild(dojo.byId('oils-selfck-circ-row'));
246
247     // items out, holds, and fines summaries
248
249     // fines summary
250     fieldmapper.standardRequest(
251         ['open-ils.actor', 'open-ils.actor.user.fines.summary'],
252         {   async : true,
253             params : [this.authtoken, this.patron.id()],
254             oncomplete : function(r) {
255                 var summary = openils.Util.readResponse(r);
256                 dojo.byId('oils-selfck-fines-total').innerHTML = 
257                     dojo.string.substitute(
258                         localeStrings.TOTAL_FINES_ACCOUNT, 
259                         [summary.balance_owed()]
260                     );
261             }
262         }
263     );
264
265     // holds summary
266     this.updateHoldsSummary();
267
268     // items out summary
269     this.updateCircSummary();
270
271     // render mock checkouts for debugging?
272     if(this.mockCheckouts) {
273         for(var i in [1,2,3]) 
274             this.displayCheckout(this.mockCheckout, 'checkout');
275     }
276 }
277
278 SelfCheckManager.prototype.updateHoldsSummary = function(decrement) {
279
280     if(!this.holdsSummary) {
281         var summary = fieldmapper.standardRequest(
282             ['open-ils.circ', 'open-ils.circ.holds.user_summary'],
283             {params : [this.authtoken, this.patron.id()]}
284         );
285
286         this.holdsSummary = {};
287         this.holdsSummary.ready = Number(summary['4']);
288         this.holdsSummary.total = 0;
289
290         for(var i in summary) 
291             this.holdsSummary.total += Number(summary[i]);
292     }
293
294     if(this.decrement) 
295         this.holdsSummary.ready -= 1;
296
297     dojo.byId('oils-selfck-holds-total').innerHTML = 
298         dojo.string.substitute(
299             localeStrings.TOTAL_HOLDS, 
300             [this.holdsSummary.total]
301         );
302
303     dojo.byId('oils-selfck-holds-ready').innerHTML = 
304         dojo.string.substitute(
305             localeStrings.HOLDS_READY_FOR_PICKUP, 
306             [this.holdsSummary.ready]
307         );
308 }
309
310
311 SelfCheckManager.prototype.updateCircSummary = function(increment) {
312
313     if(!this.circSummary) {
314
315         var summary = fieldmapper.standardRequest(
316             ['open-ils.actor', 'open-ils.actor.user.checked_out.count'],
317             {params : [this.authtoken, this.patron.id()]}
318         );
319
320         this.circSummary = {
321             total : Number(summary.out) + Number(summary.overdue),
322             overdue : Number(summary.overdue),
323             session : 0
324         };
325     }
326
327     if(increment) {
328         // local checkout occurred.  Add to the total and the session.
329         this.circSummary.total += 1;
330         this.circSummary.session += 1;
331     }
332
333     dojo.byId('oils-selfck-circ-account-total').innerHTML = 
334         dojo.string.substitute(
335             localeStrings.TOTAL_ITEMS_ACCOUNT, 
336             [this.circSummary.total]
337         );
338
339     dojo.byId('oils-selfck-circ-session-total').innerHTML = 
340         dojo.string.substitute(
341             localeStrings.TOTAL_ITEMS_SESSION, 
342             [this.circSummary.session]
343         );
344 }
345
346
347 SelfCheckManager.prototype.drawHoldsPage = function() {
348
349     // TODO add option to hid scanBox
350     // this.updateScanBox(...)
351
352     openils.Util.hide('oils-selfck-circ-page');
353     openils.Util.hide('oils-selfck-payment-page');
354     openils.Util.show('oils-selfck-holds-page');
355
356     this.holdTbody = dojo.byId('oils-selfck-hold-tbody');
357     if(!this.holdTemplate)
358         this.holdTemplate = this.holdTbody.removeChild(dojo.byId('oils-selfck-hold-row'));
359     while(this.holdTbody.childNodes[0])
360         this.holdTbody.removeChild(this.holdTbody.childNodes[0]);
361
362     progressDialog.show(true);
363
364     var self = this;
365     fieldmapper.standardRequest( // fetch the hold IDs
366
367         ['open-ils.circ', 'open-ils.circ.holds.id_list.retrieve'],
368         {   async : true,
369             params : [this.authtoken, this.patron.id()],
370
371             oncomplete : function(r) { 
372                 var ids = openils.Util.readResponse(r);
373                 if(!ids || ids.length == 0) {
374                     progressDialog.hide();
375                     return;
376                 }
377
378                 fieldmapper.standardRequest( // fetch the hold objects with fleshed details
379                     ['open-ils.circ', 'open-ils.circ.hold.details.batch.retrieve.atomic'],
380                     {   async : true,
381                         params : [self.authtoken, ids],
382
383                         oncomplete : function(rr) {
384                             self.drawHolds(openils.Util.readResponse(rr));
385                         }
386                     }
387                 );
388             }
389         }
390     );
391 }
392
393 /**
394  * Fetch and add a single hold to the list of holds
395  */
396 SelfCheckManager.prototype.drawHolds = function(holds) {
397
398     holds = holds.sort(
399         // sort available holds to the top of the list
400         // followed by queue position order
401         function(a, b) {
402             if(a.status == 4) return -1;
403             if(a.queue_position < b.queue_position) return -1;
404             return 1;
405         }
406     );
407
408     progressDialog.hide();
409
410     for(var i in holds) {
411
412         var data = holds[i];
413         var row = this.holdTemplate.cloneNode(true);
414
415         if(data.mvr.isbn()) {
416             this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + data.mvr.isbn());
417         }
418
419         this.byName(row, 'title').innerHTML = data.mvr.title();
420         this.byName(row, 'author').innerHTML = data.mvr.author();
421
422         if(data.status == 4) {
423
424             // hold is ready for pickup
425             this.byName(row, 'status').innerHTML = localeStrings.HOLD_STATUS_READY;
426
427         } else {
428
429             // hold is still pending
430             this.byName(row, 'status').innerHTML = 
431                 dojo.string.substitute(
432                     localeStrings.HOLD_STATUS_WAITING,
433                     [data.queue_position, data.potential_copies]
434                 );
435         }
436
437         this.holdTbody.appendChild(row);
438     }
439 }
440
441
442
443 /**
444  * Check out a single item.  If the item is already checked 
445  * out to the patron, redirect to renew()
446  */
447 SelfCheckManager.prototype.checkout = function(barcode, override) {
448
449     if(!barcode) {
450         this.updateScanbox(null, true);
451         return;
452     }
453
454     if(this.mockCheckouts) {
455         // if we're in mock-checkout mode, just insert another
456         // fake circ into the table and get out of here.
457         this.displayCheckout(this.mockCheckout, 'checkout');
458         return;
459     }
460
461     // TODO see if it's a patron barcode
462     // TODO see if this item has already been checked out in this session
463
464     var method = 'open-ils.circ.checkout.full';
465     if(override) method += '.override';
466
467     console.log("Checkout out item " + barcode + " with method " + method);
468
469     var result = fieldmapper.standardRequest(
470         ['open-ils.circ', 'open-ils.circ.checkout.full'],
471         {params: [
472             this.authtoken, {
473                 patron_id : this.patron.id(),
474                 copy_barcode : barcode
475             }
476         ]}
477     );
478
479     var stat = this.handleXactResult('checkout', barcode, result);
480
481     if(stat.override) {
482         this.checkout(barcode, true);
483     } else if(stat.renew) {
484         // TODO check org setting for auto-renewal interval
485         this.renew(barcode);
486     }
487 }
488
489
490 SelfCheckManager.prototype.handleXactResult = function(action, item, result) {
491
492     var displayText = '';
493     var popup = false;
494
495     // TODO handle lost/missing/etc checkin+checkout override steps
496         
497     if(result.textcode == 'NO_SESSION') {
498
499         return this.logoutStaff();
500
501     } else if(result.textcode == 'SUCCESS') {
502
503         if(action == 'checkout') {
504
505             displayText = dojo.string.substitute(
506                 localeStrings.CHECKOUT_SUCCESS, [item]);
507                 this.displayCheckout(result, 'checkout');
508
509         } else if(action == 'renew') {
510
511             displayText = dojo.string.substitute(
512                 localeStrings.RENEW_SUCCESS, [item]);
513                 this.displayCheckout(result, 'renew');
514         }
515
516         this.updateScanBox();
517
518     } else if(result.textcode == 'OPEN_CIRCULATION_EXISTS' && action == 'checkout') {
519
520         return { renew : true };
521
522     } else {
523
524         var overrideEvents = this.orgSettings[SET_AUTO_OVERRIDE_EVENTS];
525     
526         if(overrideEvents && overrideEvents.length) {
527             
528             // see if the events we received are all in the list of
529             // events to override
530     
531             if(!result.length) result = [result];
532     
533             var override = true;
534             for(var i = 0; i < result.length; i++) {
535                 var match = overrideEvents.filter(
536                     function(e) { return (e == result[i].textcode); })[0];
537                 if(!match) {
538                     override = false;
539                     break;
540                 }
541             }
542
543             if(override) 
544                 return { override : true };
545         }
546     
547         this.updateScanBox({select : true});
548         popup = true;
549
550         if(result.length) 
551             result = result[0];
552
553         switch(result.textcode) {
554
555             case 'ACTOR_USER_NOT_FOUND' : 
556                 displayText = dojo.string.substitute(
557                     localeStrings.LOGIN_FAILED, [item]);
558                 break;
559
560             case 'MAX_RENEWALS_REACHED' :
561                 displayText = dojo.string.substitute(
562                     localeStrings.MAX_RENEWALS, [item]);
563                 break;
564
565             case 'ITEM_NOT_CATALOGED' :
566                 displayText = dojo.string.substitute(
567                     localeStrings.ITEM_NOT_CATALOGED, [item]);
568                 break;
569
570             case 'already-out' : 
571                 displayText = dojo.string.substitute(
572                     localeStrings.ALREADY_OUT, [item]);
573                 break;
574
575             default:
576                 console.error('Unhandled event ' + result.textcode);
577
578                 if(action == 'checkout' || action == 'renew') {
579                     displayText = dojo.string.substitute(
580                         localeStrings.GENERIC_CIRC_FAILURE, [item]);
581                 } else {
582                     displayText = dojo.string.substitute(
583                         localeStrings.UNKNOWN_ERROR, [result.textcode]);
584                 }
585         }
586     }
587
588     console.log("Updating status with " + displayText);
589
590     dojo.byId('oils-selfck-status-div').innerHTML = displayText;
591
592     if(popup && this.orgSettings[SET_ALERT_ON_CHECKOUT_EVENT]) 
593         alert(displayText);
594
595     return {};
596 }
597
598
599 /**
600  * Renew an item
601  */
602 SelfCheckManager.prototype.renew = function(barcode, override) {
603
604     var method = 'open-ils.circ.renew';
605     if(override) method += '.override';
606
607     console.log("Renewing item " + barcode + " with method " + method);
608
609     var result = fieldmapper.standardRequest(
610         ['open-ils.circ', method],
611         {params: [
612             this.authtoken, {
613                 patron_id : this.patron.id(),
614                 copy_barcode : barcode
615             }
616         ]}
617     );
618
619     var stat = this.handleXactResult('renew', barcode, result);
620
621     if(stat.override)
622         this.renew(barcode, true);
623 }
624
625 /**
626  * Display the result of a checkout or renewal in the items out table
627  */
628 SelfCheckManager.prototype.displayCheckout = function(evt, type) {
629
630     var copy = evt.payload.copy;
631     var record = evt.payload.record;
632     var circ = evt.payload.circ;
633     var row = this.circTemplate.cloneNode(true);
634
635     if(record.isbn()) {
636         this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + record.isbn());
637     }
638
639     this.byName(row, 'barcode').innerHTML = copy.barcode();
640     this.byName(row, 'title').innerHTML = record.title();
641     this.byName(row, 'author').innerHTML = record.author();
642     this.byName(row, 'remaining').innerHTML = circ.renewal_remaining();
643     openils.Util.show(this.byName(row, type));
644
645     var date = dojo.date.stamp.fromISOString(circ.due_date());
646     this.byName(row, 'due_date').innerHTML = 
647         dojo.date.locale.format(date, {selector : 'date'});
648
649     // put new circs at the top of the list
650     this.circTbody.insertBefore(row, this.circTbody.getElementsByTagName('tr')[0]);
651 }
652
653
654 SelfCheckManager.prototype.byName = function(node, name) {
655     return dojo.query('[name=' + name+']', node)[0];
656 }
657
658
659 SelfCheckManager.prototype.drawFinesPage = function() {
660
661     openils.Util.hide('oils-selfck-circ-page');
662     openils.Util.hide('oils-selfck-holds-page');
663     openils.Util.show('oils-selfck-payment-page');
664
665 }
666
667 /**
668  * Print a receipt
669  */
670 SelfCheckManager.prototype.printReceipt = function() {
671 }
672
673
674 /**
675  * Logout the patron and return to the login page
676  */
677 SelfCheckManager.prototype.logoutPatron = function() {
678
679     this.patron = null;
680     this.holdsSummary = null;
681     this.circSummary = null;
682
683     this.drawLoginPage();
684 }
685
686
687 /**
688  * Fire up the manager on page load
689  */
690 openils.Util.addOnLoad(
691     function() {
692         new SelfCheckManager().init();
693     }
694 );