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