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