]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/circ/selfcheck/selfcheck.js
implemented items out page. made printing of top-level receipt optional. trimmed...
[working/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
34     // During renewals, keep track of the ID of the previous circulation. 
35     // Previous circ is used for tracking failed renewals (for receipts).
36     this.prevCirc = null;
37
38     // current item barcode
39     this.itemBarcode = null; 
40
41     // are we currently performing a renewal?
42     this.isRenewal = false; 
43
44     // dict of org unit settings for "here"
45     this.orgSettings = {};
46
47     // Construct a mock checkout for debugging purposes
48     if(this.mockCheckouts = this.cgi.param('mock-circ')) {
49
50         this.mockCheckout = {
51             payload : {
52                 record : new fieldmapper.mvr(),
53                 copy : new fieldmapper.acp(),
54                 circ : new fieldmapper.circ()
55             }
56         };
57
58         this.mockCheckout.payload.record.title('Jazz improvisation for guitar');
59         this.mockCheckout.payload.record.author('Wise, Les');
60         this.mockCheckout.payload.record.isbn('0634033565');
61         this.mockCheckout.payload.copy.barcode('123456789');
62         this.mockCheckout.payload.circ.renewal_remaining(1);
63         this.mockCheckout.payload.circ.parent_circ(1);
64         this.mockCheckout.payload.circ.due_date('2012-12-21');
65     }
66 }
67
68
69
70 /**
71  * Fetch the org-unit settings, initialize the display, etc.
72  */
73 SelfCheckManager.prototype.init = function() {
74
75     this.staff = openils.User.user;
76     this.workstation = openils.User.workstation;
77     this.authtoken = openils.User.authtoken;
78     this.loadOrgSettings();
79
80     this.circTbody = dojo.byId('oils-selfck-circ-tbody');
81     this.itemsOutTbody = dojo.byId('oils-selfck-circ-out-tbody');
82
83     // workstation is required but none provided
84     if(this.orgSettings[SET_WORKSTATION_REQUIRED] && !this.workstation) {
85         alert(dojo.string.substitute(localeStrings.WORKSTATION_REQUIRED));
86         return;
87     }
88     
89     var self = this;
90     // connect onclick handlers to the various navigation links
91     var linkHandlers = {
92         'oils-selfck-hold-details-link' : function() { self.drawHoldsPage(); },
93         //'oils-selfck-nav-holds' : function() { self.drawHoldsPage(); },
94         'oils-selfck-pay-fines-link' : function() { self.drawFinesPage(); },
95         //'oils-selfck-nav-fines' : function() { self.drawFinesPage(); },
96         'oils-selfck-nav-home' : function() { self.drawCircPage(); },
97         'oils-selfck-nav-logout' : function() { self.logoutPatron(); },
98         'oils-selfck-nav-logout-print' : function() { self.logoutPatron(true); },
99         'oils-selfck-items-out-details-link' : function() { self.drawItemsOutPage(); }
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.printReceipt();
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
303     while(this.itemsOutTbody.childNodes[0])
304         this.itemsOutTbody.removeChild(this.itemsOutTbody.childNodes[0]);
305
306     var self = this;
307     this.updateScanBox({
308         msg : 'Please enter an item barcode', // TODO i18n
309         handler : function(barcode) { self.checkout(barcode); }
310     });
311
312     openils.Util.hide('oils-selfck-payment-page');
313     openils.Util.hide('oils-selfck-holds-page');
314     openils.Util.show('oils-selfck-circ-page');
315
316     if(!this.circTemplate)
317         this.circTemplate = this.circTbody.removeChild(dojo.byId('oils-selfck-circ-row'));
318
319     // items out, holds, and fines summaries
320
321     // fines summary
322     fieldmapper.standardRequest(
323         ['open-ils.actor', 'open-ils.actor.user.fines.summary'],
324         {   async : true,
325             params : [this.authtoken, this.patron.id()],
326             oncomplete : function(r) {
327                 var summary = openils.Util.readResponse(r);
328                 dojo.byId('oils-selfck-fines-total').innerHTML = 
329                     dojo.string.substitute(
330                         localeStrings.TOTAL_FINES_ACCOUNT, 
331                         [summary.balance_owed()]
332                     );
333             }
334         }
335     );
336
337     // holds summary
338     this.updateHoldsSummary();
339
340     // items out summary
341     this.updateCircSummary();
342
343     // render mock checkouts for debugging?
344     if(this.mockCheckouts) {
345         for(var i in [1,2,3]) 
346             this.displayCheckout(this.mockCheckout, 'checkout');
347     }
348 }
349
350 SelfCheckManager.prototype.drawItemsOutPage = function() {
351     openils.Util.hide('oils-selfck-circ-tbody');
352     openils.Util.hide('oils-selfck-payment-page');
353     openils.Util.hide('oils-selfck-holds-page');
354     openils.Util.show('oils-selfck-circ-page');
355
356     while(this.itemsOutTbody.childNodes[0])
357         this.itemsOutTbody.removeChild(this.itemsOutTbody.childNodes[0]);
358
359     progressDialog.show(true);
360     
361     var self = this;
362     fieldmapper.standardRequest(
363         ['open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic'],
364         {
365             async : true,
366             params : [this.authtoken, this.patron.id()],
367             oncomplete : function(r) {
368
369                 var resp = openils.Util.readResponse(r);
370
371                 var circs = resp.sort(
372                     function(a, b) {
373                         if(a.circ.due_date() > b.circ.due_date())
374                             return -1;
375                         return 1;
376                     }
377                 );
378
379                 progressDialog.hide();
380
381                 dojo.forEach(circs,
382                     function(circ) {
383                         self.displayCheckout(
384                             {payload : circ}, 
385                             (circ.circ.parent_circ()) ? 'renew' : 'checkout',
386                             true
387                         );
388                     }
389                 );
390             }
391         }
392     );
393 }
394
395 SelfCheckManager.prototype.updateHoldsSummary = function() {
396
397     if(!this.holdsSummary) {
398         var summary = fieldmapper.standardRequest(
399             ['open-ils.circ', 'open-ils.circ.holds.user_summary'],
400             {params : [this.authtoken, this.patron.id()]}
401         );
402
403         this.holdsSummary = {};
404         this.holdsSummary.ready = Number(summary['4']);
405         this.holdsSummary.total = 0;
406
407         for(var i in summary) 
408             this.holdsSummary.total += Number(summary[i]);
409     }
410
411     dojo.byId('oils-selfck-holds-total').innerHTML = 
412         dojo.string.substitute(
413             localeStrings.TOTAL_HOLDS, 
414             [this.holdsSummary.total]
415         );
416
417     dojo.byId('oils-selfck-holds-ready').innerHTML = 
418         dojo.string.substitute(
419             localeStrings.HOLDS_READY_FOR_PICKUP, 
420             [this.holdsSummary.ready]
421         );
422 }
423
424
425 SelfCheckManager.prototype.updateCircSummary = function(increment) {
426
427     if(!this.circSummary) {
428
429         var summary = fieldmapper.standardRequest(
430             ['open-ils.actor', 'open-ils.actor.user.checked_out.count'],
431             {params : [this.authtoken, this.patron.id()]}
432         );
433
434         this.circSummary = {
435             total : Number(summary.out) + Number(summary.overdue),
436             overdue : Number(summary.overdue),
437             session : 0
438         };
439     }
440
441     if(increment) {
442         // local checkout occurred.  Add to the total and the session.
443         this.circSummary.total += 1;
444         this.circSummary.session += 1;
445     }
446
447     dojo.byId('oils-selfck-circ-account-total').innerHTML = 
448         dojo.string.substitute(
449             localeStrings.TOTAL_ITEMS_ACCOUNT, 
450             [this.circSummary.total]
451         );
452
453     dojo.byId('oils-selfck-circ-session-total').innerHTML = 
454         dojo.string.substitute(
455             localeStrings.TOTAL_ITEMS_SESSION, 
456             [this.circSummary.session]
457         );
458 }
459
460
461 SelfCheckManager.prototype.drawHoldsPage = function() {
462
463     // TODO add option to hid scanBox
464     // this.updateScanBox(...)
465
466     openils.Util.hide('oils-selfck-circ-page');
467     openils.Util.hide('oils-selfck-payment-page');
468     openils.Util.show('oils-selfck-holds-page');
469
470     this.holdTbody = dojo.byId('oils-selfck-hold-tbody');
471     if(!this.holdTemplate)
472         this.holdTemplate = this.holdTbody.removeChild(dojo.byId('oils-selfck-hold-row'));
473     while(this.holdTbody.childNodes[0])
474         this.holdTbody.removeChild(this.holdTbody.childNodes[0]);
475
476     progressDialog.show(true);
477
478     var self = this;
479     fieldmapper.standardRequest( // fetch the hold IDs
480
481         ['open-ils.circ', 'open-ils.circ.holds.id_list.retrieve'],
482         {   async : true,
483             params : [this.authtoken, this.patron.id()],
484
485             oncomplete : function(r) { 
486                 var ids = openils.Util.readResponse(r);
487                 if(!ids || ids.length == 0) {
488                     progressDialog.hide();
489                     return;
490                 }
491
492                 fieldmapper.standardRequest( // fetch the hold objects with fleshed details
493                     ['open-ils.circ', 'open-ils.circ.hold.details.batch.retrieve.atomic'],
494                     {   async : true,
495                         params : [self.authtoken, ids],
496
497                         oncomplete : function(rr) {
498                             self.drawHolds(openils.Util.readResponse(rr));
499                         }
500                     }
501                 );
502             }
503         }
504     );
505 }
506
507 /**
508  * Fetch and add a single hold to the list of holds
509  */
510 SelfCheckManager.prototype.drawHolds = function(holds) {
511
512     holds = holds.sort(
513         // sort available holds to the top of the list
514         // followed by queue position order
515         function(a, b) {
516             if(a.status == 4) return -1;
517             if(a.queue_position < b.queue_position) return -1;
518             return 1;
519         }
520     );
521
522     progressDialog.hide();
523
524     for(var i in holds) {
525
526         var data = holds[i];
527         var row = this.holdTemplate.cloneNode(true);
528
529         if(data.mvr.isbn()) {
530             this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + data.mvr.isbn());
531         }
532
533         this.byName(row, 'title').innerHTML = data.mvr.title();
534         this.byName(row, 'author').innerHTML = data.mvr.author();
535
536         if(data.status == 4) {
537
538             // hold is ready for pickup
539             this.byName(row, 'status').innerHTML = localeStrings.HOLD_STATUS_READY;
540
541         } else {
542
543             // hold is still pending
544             this.byName(row, 'status').innerHTML = 
545                 dojo.string.substitute(
546                     localeStrings.HOLD_STATUS_WAITING,
547                     [data.queue_position, data.potential_copies]
548                 );
549         }
550
551         this.holdTbody.appendChild(row);
552     }
553 }
554
555
556
557 /**
558  * Check out a single item.  If the item is already checked 
559  * out to the patron, redirect to renew()
560  */
561 SelfCheckManager.prototype.checkout = function(barcode, override) {
562
563     this.prevCirc = null;
564
565     if(!barcode) {
566         this.updateScanbox(null, true);
567         return;
568     }
569
570     if(this.mockCheckouts) {
571         // if we're in mock-checkout mode, just insert another
572         // fake circ into the table and get out of here.
573         this.displayCheckout(this.mockCheckout, 'checkout');
574         return;
575     }
576
577     // TODO see if it's a patron barcode
578     // TODO see if this item has already been checked out in this session
579
580     var method = 'open-ils.circ.checkout.full';
581     if(override) method += '.override';
582
583     console.log("Checkout out item " + barcode + " with method " + method);
584
585     var result = fieldmapper.standardRequest(
586         ['open-ils.circ', 'open-ils.circ.checkout.full'],
587         {params: [
588             this.authtoken, {
589                 patron_id : this.patron.id(),
590                 copy_barcode : barcode
591             }
592         ]}
593     );
594
595     console.log(js2JSON(result));
596
597     var stat = this.handleXactResult('checkout', barcode, result);
598
599     if(stat.override) {
600         this.checkout(barcode, true);
601     } else if(stat.renew) {
602         this.renew(barcode);
603     }
604 }
605
606
607 SelfCheckManager.prototype.handleXactResult = function(action, item, result) {
608
609     var displayText = '';
610
611     // If true, the display message is important enough to pop up.  Whether or not
612     // an alert() actually occurs, depends on org unit settings
613     var popup = false;  
614
615     var sound = '';
616
617     // TODO handle lost/missing/etc checkin+checkout override steps
618     
619     var payload = result.payload || {};
620         
621     if(result.textcode == 'NO_SESSION') {
622
623         return this.logoutStaff();
624
625     } else if(result.textcode == 'SUCCESS') {
626
627         if(action == 'checkout') {
628
629             displayText = dojo.string.substitute(localeStrings.CHECKOUT_SUCCESS, [item]);
630             this.displayCheckout(result, 'checkout');
631
632             if(payload.holds_fulfilled && payload.holds_fulfilled.length) {
633                 // A hold was fulfilled, update the hold numbers in the circ summary
634                 console.log("fulfilled hold " + payload.holds_fulfilled + " during checkout");
635                 this.holdsSummary = null;
636                 this.updateHoldsSummary();
637             }
638
639             this.updateCircSummary(true);
640
641         } else if(action == 'renew') {
642
643             displayText = dojo.string.substitute(localeStrings.RENEW_SUCCESS, [item]);
644             this.displayCheckout(result, 'renew');
645         }
646
647         this.checkouts.push({circ : result.payload.circ.id()});
648         sound = 'checkout-success';
649         this.updateScanBox();
650
651     } else if(result.textcode == 'OPEN_CIRCULATION_EXISTS' && action == 'checkout') {
652
653         // Server says the item is already checked out.  If it's checked out to the
654         // current user, we may need to renew it.  
655
656         if(payload.old_circ) { 
657
658             /*
659             old_circ refers to the previous checkout IFF it's for the same user. 
660             If no auto-renew interval is not defined, assume we should renew it
661             If an auto-renew interval is defined and the payload comes back with
662             auto_renew set to true, do the renewal.  Otherwise, let the patron know
663             the item is already checked out to them.  */
664
665             if( !this.orgSettings[SET_AUTO_RENEW_INTERVAL] ||
666                 (this.orgSettings[SET_AUTO_RENEW_INTERVAL] && payload.auto_renew) ) {
667                 this.prevCirc = payload.old_circ.id();
668                 return { renew : true };
669             }
670
671             popup = true;
672             sound = 'checkout-failure';
673             displayText = dojo.string.substitute(localeStrings.ALREADY_OUT, [item]);
674
675         } else {
676             
677             // item is checked out to some other user
678             popup = true;
679             sound = 'checkout-failure';
680             displayText = dojo.string.substitute(localeStrings.OPEN_CIRCULATION_EXISTS, [item]);
681         }
682
683         this.updateScanBox({select:true});
684
685     } else {
686
687         var overrideEvents = this.orgSettings[SET_AUTO_OVERRIDE_EVENTS];
688     
689         if(overrideEvents && overrideEvents.length) {
690             
691             // see if the events we received are all in the list of
692             // events to override
693     
694             if(!result.length) result = [result];
695     
696             var override = true;
697             for(var i = 0; i < result.length; i++) {
698                 var match = overrideEvents.filter(
699                     function(e) { return (e == result[i].textcode); })[0];
700                 if(!match) {
701                     override = false;
702                     break;
703                 }
704             }
705
706             if(override) 
707                 return { override : true };
708         }
709     
710         this.updateScanBox({select : true});
711         popup = true;
712         sound = 'checkout-failure';
713
714         if(action == 'renew')
715             this.checkouts.push({circ : this.prevCirc, renewal_failure : true});
716
717         if(result.length) 
718             result = result[0];
719
720         switch(result.textcode) {
721
722             // TODO custom handler for blocking penalties
723
724             case 'MAX_RENEWALS_REACHED' :
725                 displayText = dojo.string.substitute(
726                     localeStrings.MAX_RENEWALS, [item]);
727                 break;
728
729             case 'ITEM_NOT_CATALOGED' :
730                 displayText = dojo.string.substitute(
731                     localeStrings.ITEM_NOT_CATALOGED, [item]);
732                 break;
733
734             case 'OPEN_CIRCULATION_EXISTS' :
735                 displayText = dojo.string.substitute(
736                     localeStrings.OPEN_CIRCULATION_EXISTS, [item]);
737                 break;
738
739             default:
740                 console.error('Unhandled event ' + result.textcode);
741
742                 if(action == 'checkout' || action == 'renew') {
743                     displayText = dojo.string.substitute(
744                         localeStrings.GENERIC_CIRC_FAILURE, [item]);
745                 } else {
746                     displayText = dojo.string.substitute(
747                         localeStrings.UNKNOWN_ERROR, [result.textcode]);
748                 }
749         }
750     }
751
752     this.handleAlert(displayText, popup, sound);
753     return {};
754 }
755
756
757 /**
758  * Renew an item
759  */
760 SelfCheckManager.prototype.renew = function(barcode, override) {
761
762     var method = 'open-ils.circ.renew';
763     if(override) method += '.override';
764
765     console.log("Renewing item " + barcode + " with method " + method);
766
767     var result = fieldmapper.standardRequest(
768         ['open-ils.circ', method],
769         {params: [
770             this.authtoken, {
771                 patron_id : this.patron.id(),
772                 copy_barcode : barcode
773             }
774         ]}
775     );
776
777     console.log(js2JSON(result));
778
779     var stat = this.handleXactResult('renew', barcode, result);
780
781     if(stat.override)
782         this.renew(barcode, true);
783 }
784
785 /**
786  * Display the result of a checkout or renewal in the items out table
787  */
788 SelfCheckManager.prototype.displayCheckout = function(evt, type, itemsOut) {
789
790     var copy = evt.payload.copy;
791     var record = evt.payload.record;
792     var circ = evt.payload.circ;
793     var row = this.circTemplate.cloneNode(true);
794
795     if(record.isbn()) {
796         this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + record.isbn());
797     }
798
799     this.byName(row, 'barcode').innerHTML = copy.barcode();
800     this.byName(row, 'title').innerHTML = record.title();
801     this.byName(row, 'author').innerHTML = record.author();
802     this.byName(row, 'remaining').innerHTML = circ.renewal_remaining();
803     openils.Util.show(this.byName(row, type));
804
805     var date = dojo.date.stamp.fromISOString(circ.due_date());
806     this.byName(row, 'due_date').innerHTML = 
807         dojo.date.locale.format(date, {selector : 'date'});
808
809     // put new circs at the top of the list
810     var tbody = this.circTbody;
811     if(itemsOut) tbody = this.itemsOutTbody;
812     tbody.insertBefore(row, tbody.getElementsByTagName('tr')[0]);
813 }
814
815
816 SelfCheckManager.prototype.byName = function(node, name) {
817     return dojo.query('[name=' + name+']', node)[0];
818 }
819
820
821 SelfCheckManager.prototype.drawFinesPage = function() {
822     openils.Util.hide('oils-selfck-circ-page');
823     openils.Util.hide('oils-selfck-holds-page');
824     openils.Util.show('oils-selfck-payment-page');
825 }
826
827
828 SelfCheckManager.prototype.initPrinter = function() {
829     try { // Mozilla only
830                 netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
831         netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
832         netscape.security.PrivilegeManager.enablePrivilege('UniversalPreferencesRead');
833         netscape.security.PrivilegeManager.enablePrivilege('UniversalPreferencesWrite');
834         var pref = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
835         if (pref)
836             pref.setBoolPref('print.always_print_silent', true);
837     } catch(E) {
838         console.log("Unable to initialize auto-printing"); 
839     }
840 }
841
842 /**
843  * Print a receipt
844  */
845 SelfCheckManager.prototype.printReceipt = function(callback) {
846
847     var circIds = [];
848     var circCtx = []; // circ context data.  in this case, renewal_failure info
849
850     // collect the circs and failure info
851     dojo.forEach(
852         this.checkouts, 
853         function(blob) {
854             circIds.push(blob.circ);
855             circCtx.push({renewal_failure:blob.renewal_failure});
856         }
857     );
858
859     var params = [
860         this.authtoken, 
861         this.staff.ws_ou(),
862         null,
863         'format.selfcheck.checkout',
864         'print-on-demand',
865         circIds,
866         circCtx
867     ];
868
869     var self = this;
870     fieldmapper.standardRequest(
871         ['open-ils.circ', 'open-ils.circ.fire_circ_trigger_events'],
872         {   
873             async : true,
874             params : params,
875             oncomplete : function(r) {
876                 var resp = openils.Util.readResponse(r);
877                 var output = resp.template_output();
878                 if(output) {
879                     self.printData(output.data(), self.checkouts.length, callback); 
880                 } else {
881                     var error = resp.error_output();
882                     if(error) {
883                         throw new Error("Error creating receipt: " + error.data());
884                     } else {
885                         throw new Error("No receipt data returned from server");
886                     }
887                 }
888             }
889         }
890     );
891 }
892
893 SelfCheckManager.prototype.printData = function(data, numItems, callback) {
894
895     var win = window.open('', '', 'resizable,width=700,height=500,scrollbars=1'); 
896     win.document.body.innerHTML = data;
897     win.print();
898
899     /*
900      * There is no way to know when the browser is done printing.
901      * Make a best guess at when to close the print window by basing
902      * the setTimeout wait on the number of items to be printed plus
903      * a small buffer
904      */
905     var sleepTime = 1000;
906     if(numItems > 0) 
907         sleepTime += (numItems / 2) * 1000;
908
909     setTimeout(
910         function() { 
911             win.close(); // close the print window
912             if(callback)
913                 callback(); // fire optional post-print callback
914         },
915         sleepTime 
916     );
917 }
918
919
920
921 /**
922  * Logout the patron and return to the login page
923  */
924 SelfCheckManager.prototype.logoutPatron = function(print) {
925     if(print && this.checkouts.length) {
926         this.printReceipt(
927             function() {
928                 location.href = location.href;
929             }
930         );
931     } else {
932         location.href = location.href;
933     }
934 }
935
936
937 /**
938  * Fire up the manager on page load
939  */
940 openils.Util.addOnLoad(
941     function() {
942         new SelfCheckManager().init();
943     }
944 );