]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/circ/selfcheck/selfcheck.js
f721af00058d36a30119e37f3caee1c4687178b1
[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
196     if(args.select) {
197         selfckScanBox.domNode.select();
198     } else {
199         selfckScanBox.attr('value', '');
200     }
201
202     if(args.value)
203         selfckScanBox.attr('value', args.value);
204
205     if(args.msg) 
206         dojo.byId('oils-selfck-scan-text').innerHTML = args.msg;
207
208     if(selfckScanBox._lastHandler && (args.handler || args.clearHandler)) {
209         dojo.disconnect(selfckScanBox._lastHandler);
210     }
211
212     if(args.handler) {
213         selfckScanBox._lastHandler = dojo.connect(
214             selfckScanBox, 
215             'onKeyDown', 
216             function(e) {
217                 if(e.keyCode != dojo.keys.ENTER) 
218                     return;
219                 args.handler(selfckScanBox.attr('value'));
220             }
221         );
222     }
223
224     selfckScanBox.focus();
225 }
226
227 /**
228  *  Sets up the checkout/renewal interface
229  */
230 SelfCheckManager.prototype.drawCircPage = function() {
231
232     var self = this;
233     this.updateScanBox({
234         msg : 'Please enter an item barcode', // TODO i18n
235         handler : function(barcode) { self.checkout(barcode); }
236     });
237
238     openils.Util.hide('oils-selfck-payment-page');
239     openils.Util.hide('oils-selfck-holds-page');
240     openils.Util.show('oils-selfck-circ-page');
241
242     this.circTbody = dojo.byId('oils-selfck-circ-tbody');
243     if(!this.circTemplate)
244         this.circTemplate = this.circTbody.removeChild(dojo.byId('oils-selfck-circ-row'));
245
246     // items out, holds, and fines summaries
247
248     // fines summary
249     fieldmapper.standardRequest(
250         ['open-ils.actor', 'open-ils.actor.user.fines.summary'],
251         {   async : true,
252             params : [this.authtoken, this.patron.id()],
253             oncomplete : function(r) {
254                 var summary = openils.Util.readResponse(r);
255                 dojo.byId('oils-selfck-fines-total').innerHTML = 
256                     dojo.string.substitute(
257                         localeStrings.TOTAL_FINES_ACCOUNT, 
258                         [summary.balance_owed()]
259                     );
260             }
261         }
262     );
263
264     // holds summary
265     this.updateHoldsSummary();
266
267     // items out summary
268     this.updateCircSummary();
269
270     // render mock checkouts for debugging?
271     if(this.mockCheckouts) {
272         for(var i in [1,2,3]) 
273             this.displayCheckout(this.mockCheckout);
274     }
275 }
276
277 SelfCheckManager.prototype.updateHoldsSummary = function(decrement) {
278
279     if(!this.holdsSummary) {
280         var summary = fieldmapper.standardRequest(
281             ['open-ils.circ', 'open-ils.circ.holds.user_summary'],
282             {params : [this.authtoken, this.patron.id()]}
283         );
284
285         this.holdsSummary = {};
286         this.holdsSummary.ready = Number(summary['4']);
287         this.holdsSummary.total = 0;
288
289         for(var i in summary) 
290             this.holdsSummary.total += Number(summary[i]);
291     }
292
293     if(this.decrement) 
294         this.holdsSummary.ready -= 1;
295
296     dojo.byId('oils-selfck-holds-total').innerHTML = 
297         dojo.string.substitute(
298             localeStrings.TOTAL_HOLDS, 
299             [this.holdsSummary.total]
300         );
301
302     dojo.byId('oils-selfck-holds-ready').innerHTML = 
303         dojo.string.substitute(
304             localeStrings.HOLDS_READY_FOR_PICKUP, 
305             [this.holdsSummary.ready]
306         );
307 }
308
309
310 SelfCheckManager.prototype.updateCircSummary = function(increment) {
311
312     if(!this.circSummary) {
313
314         var summary = fieldmapper.standardRequest(
315             ['open-ils.actor', 'open-ils.actor.user.checked_out.count'],
316             {params : [this.authtoken, this.patron.id()]}
317         );
318
319         this.circSummary = {
320             total : Number(summary.out) + Number(summary.overdue),
321             overdue : Number(summary.overdue),
322             session : 0
323         };
324     }
325
326     if(increment) {
327         // local checkout occurred.  Add to the total and the session.
328         this.circSummary.total += 1;
329         this.circSummary.session += 1;
330     }
331
332     dojo.byId('oils-selfck-circ-account-total').innerHTML = 
333         dojo.string.substitute(
334             localeStrings.TOTAL_ITEMS_ACCOUNT, 
335             [this.circSummary.total]
336         );
337
338     dojo.byId('oils-selfck-circ-session-total').innerHTML = 
339         dojo.string.substitute(
340             localeStrings.TOTAL_ITEMS_SESSION, 
341             [this.circSummary.session]
342         );
343 }
344
345
346 SelfCheckManager.prototype.drawHoldsPage = function() {
347
348     // TODO add option to hid scanBox
349     // this.updateScanBox(...)
350
351     openils.Util.hide('oils-selfck-circ-page');
352     openils.Util.hide('oils-selfck-payment-page');
353     openils.Util.show('oils-selfck-holds-page');
354
355     this.holdTbody = dojo.byId('oils-selfck-hold-tbody');
356     if(!this.holdTemplate)
357         this.holdTemplate = this.holdTbody.removeChild(dojo.byId('oils-selfck-hold-row'));
358     while(this.holdTbody.childNodes[0])
359         this.holdTbody.removeChild(this.holdTbody.childNodes[0]);
360
361     progressDialog.show(true);
362
363     var self = this;
364     fieldmapper.standardRequest( // fetch the hold IDs
365
366         ['open-ils.circ', 'open-ils.circ.holds.id_list.retrieve'],
367         {   async : true,
368             params : [this.authtoken, this.patron.id()],
369
370             oncomplete : function(r) { 
371                 var ids = openils.Util.readResponse(r);
372                 if(!ids || ids.length == 0) {
373                     progressDialog.hide();
374                     return;
375                 }
376
377                 fieldmapper.standardRequest( // fetch the hold objects with fleshed details
378                     ['open-ils.circ', 'open-ils.circ.hold.details.batch.retrieve.atomic'],
379                     {   async : true,
380                         params : [self.authtoken, ids],
381
382                         oncomplete : function(rr) {
383                             self.drawHolds(openils.Util.readResponse(rr));
384                         }
385                     }
386                 );
387             }
388         }
389     );
390 }
391
392 /**
393  * Fetch and add a single hold to the list of holds
394  */
395 SelfCheckManager.prototype.drawHolds = function(holds) {
396
397     holds = holds.sort(
398         // sort available holds to the top of the list
399         // followed by queue position order
400         function(a, b) {
401             if(a.status == 4) return -1;
402             if(a.queue_position < b.queue_position) return -1;
403             return 1;
404         }
405     );
406
407     progressDialog.hide();
408
409     for(var i in holds) {
410
411         var data = holds[i];
412         var row = this.holdTemplate.cloneNode(true);
413
414         if(data.mvr.isbn()) {
415             this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + data.mvr.isbn());
416         }
417
418         this.byName(row, 'title').innerHTML = data.mvr.title();
419         this.byName(row, 'author').innerHTML = data.mvr.author();
420
421         if(data.status == 4) {
422
423             // hold is ready for pickup
424             this.byName(row, 'status').innerHTML = localeStrings.HOLD_STATUS_READY;
425
426         } else {
427
428             // hold is still pending
429             this.byName(row, 'status').innerHTML = 
430                 dojo.string.substitute(
431                     localeStrings.HOLD_STATUS_WAITING,
432                     [data.queue_position, data.potential_copies]
433                 );
434         }
435
436         this.holdTbody.appendChild(row);
437     }
438 }
439
440
441
442 /**
443  * Check out a single item.  If the item is already checked 
444  * out to the patron, redirect to renew()
445  */
446 SelfCheckManager.prototype.checkout = function(barcode, override) {
447
448     if(!barcode) {
449         this.updateScanbox(null, true);
450         return;
451     }
452
453     if(this.mockCheckouts) {
454         // if we're in mock-checkout mode, just insert another
455         // fake circ into the table and get out of here.
456         this.displayCheckout(this.mockCheckout);
457         return;
458     }
459
460     // TODO see if it's a patron barcode
461     // TODO see if this item has already been checked out in this session
462
463     var method = 'open-ils.circ.checkout.full';
464     if(override) method += '.override';
465
466     var result = fieldmapper.standardRequest(
467         ['open-ils.circ', 'open-ils.circ.checkout.full'],
468         {params: [
469             this.authtoken, {
470                 patron_id : this.patron.id(),
471                 copy_barcode : barcode
472             }
473         ]}
474     );
475
476     var stat = this.handleXactResult('checkout', barcode, result);
477
478     console.log("Circ resulted in " + js2JSON(result));
479
480     if(stat.override)
481         this.checkout(barcode, true);
482
483 }
484
485
486 SelfCheckManager.prototype.handleXactResult = function(action, item, result) {
487
488     var displayText = '';
489     var popup = false;
490
491     // TODO handle lost/missing/etc checkin+checkout override steps
492         
493     if(result.textcode == 'NO_SESSION') {
494
495         return this.logoutStaff();
496
497     } else if(result.textcode == 'SUCCESS') {
498
499         if(action == 'checkout') {
500
501             displayText = dojo.string.substitute(
502                 localeStrings.CHECKOUT_SUCCESS, [item]);
503                 this.displayCheckout(result);
504
505         } else if(action == 'renew') {
506
507             displayText = dojo.string.substitute(
508                 localeStrings.RENEW_SUCCESS, [item]);
509                 this.displayCheckout(result);
510         }
511
512         this.updateScanBox();
513
514     } else if(result.textcode == 'OPEN_CIRCULATION_EXISTS' && action == 'checkout') {
515
516         this.renew(item);
517
518     } else {
519
520         var overrideEvents = this.orgSettings[SET_AUTO_OVERRIDE_EVENTS];
521     
522         if(overrideEvents && overrideEvents.length) {
523             
524             // see if the events we received are all in the list of
525             // events to override
526     
527             if(!result.length) result = [result];
528     
529             var override = true;
530             for(var i = 0; i < result.length; i++) {
531                 var match = overrideEvents.filter(
532                     function(e) { return (e == result[i].textcode); })[0];
533                 if(!match) {
534                     override = false;
535                     break;
536                 }
537             }
538
539             if(override) 
540                 return { override : true };
541         }
542     
543         this.updateScanBox({select : true});
544         popup = true;
545
546         if(result.length) 
547             result = result[0];
548
549         switch(result.textcode) {
550
551             case 'ACTOR_USER_NOT_FOUND' : 
552                 displayText = dojo.string.substitute(
553                     localeStrings.LOGIN_FAILED, [item]);
554                 break;
555
556             case 'already-out' : 
557                     displayText = dojo.string.substitute(
558                         localeStrings.ALREADY_OUT, [item]);
559
560             default:
561                 console.error('Unhandled event ' + result.textcode);
562
563                 if(action == 'checkout' || action == 'renew') {
564                     displayText = dojo.string.substitute(
565                         localeStrings.GENERIC_CIRC_FAILURE, [item]);
566                 } else {
567                     displayText = dojo.string.substitute(
568                         localeStrings.UNKNOWN_ERROR, [result.textcode]);
569                 }
570         }
571     }
572
573     dojo.byId('oils-selfck-status-div').innerHTML = displayText;
574
575     if(popup && this.orgSettings[SET_ALERT_ON_CHECKOUT_EVENT]) 
576         alert(displayText);
577
578     return {};
579 }
580
581
582 /**
583  * Renew an item
584  */
585 SelfCheckManager.prototype.renew = function() {
586 }
587
588 /**
589  * Display the result of a checkout or renewal in the items out table
590  */
591 SelfCheckManager.prototype.displayCheckout = function(evt) {
592
593     var copy = evt.payload.copy;
594     var record = evt.payload.record;
595     var circ = evt.payload.circ;
596     var row = this.circTemplate.cloneNode(true);
597
598     if(record.isbn()) {
599         this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + record.isbn());
600     }
601
602     this.byName(row, 'barcode').innerHTML = copy.barcode();
603     this.byName(row, 'title').innerHTML = record.title();
604     this.byName(row, 'author').innerHTML = record.author();
605     this.byName(row, 'remaining').innerHTML = circ.renewal_remaining();
606
607     var date = dojo.date.stamp.fromISOString(circ.due_date());
608     this.byName(row, 'due_date').innerHTML = 
609         dojo.date.locale.format(date, {selector : 'date'});
610
611     // put new circs at the top of the list
612     this.circTbody.insertBefore(row, this.circTbody.getElementsByTagName('tr')[0]);
613 }
614
615
616 SelfCheckManager.prototype.byName = function(node, name) {
617     return dojo.query('[name=' + name+']', node)[0];
618 }
619
620
621 SelfCheckManager.prototype.drawFinesPage = function() {
622
623     openils.Util.hide('oils-selfck-circ-page');
624     openils.Util.hide('oils-selfck-holds-page');
625     openils.Util.show('oils-selfck-payment-page');
626
627 }
628
629 /**
630  * Print a receipt
631  */
632 SelfCheckManager.prototype.printReceipt = function() {
633 }
634
635
636 /**
637  * Logout the patron and return to the login page
638  */
639 SelfCheckManager.prototype.logoutPatron = function() {
640
641     this.patron = null;
642     this.holdsSummary = null;
643     this.circSummary = null;
644
645     this.drawLoginPage();
646 }
647
648
649 /**
650  * Fire up the manager on page load
651  */
652 openils.Util.addOnLoad(
653     function() {
654         new SelfCheckManager().init();
655     }
656 );