Initial dev repository
[kcls-web.git] / js / ui / kcls / circ / selfcheck / selfcheck.js
diff --git a/js/ui/kcls/circ/selfcheck/selfcheck.js b/js/ui/kcls/circ/selfcheck/selfcheck.js
new file mode 100644 (file)
index 0000000..0859ec1
--- /dev/null
@@ -0,0 +1,1488 @@
+dojo.require('dojo.date.locale');\r
+dojo.require('dojo.date.stamp');\r
+dojo.require('dijit.form.CheckBox');\r
+dojo.require('dijit.form.NumberSpinner');\r
+dojo.require('openils.CGI');\r
+dojo.require('openils.Util');\r
+dojo.require('openils.User');\r
+dojo.require('openils.Event');\r
+dojo.require('openils.widget.ProgressDialog');\r
+dojo.require('openils.widget.OrgUnitFilteringSelect');\r
+\r
+dojo.requireLocalization('openils.circ', 'selfcheck');\r
+var localeStrings = dojo.i18n.getLocalization('openils.circ', 'selfcheck');\r
+var selfCheckMgr;\r
+var itemsOutCirc = [];\r
+var itemsOutMod = [];\r
+var itemsOutCopy = [];\r
+var TIMEOUT = 45; // logout timer\r
+\r
+\r
+const SET_BARCODE_REGEX = 'opac.barcode_regex';\r
+const SET_PATRON_TIMEOUT = 'circ.selfcheck.patron_login_timeout';\r
+const SET_AUTO_OVERRIDE_EVENTS = 'circ.selfcheck.auto_override_checkout_events';\r
+const SET_PATRON_PASSWORD_REQUIRED = 'circ.selfcheck.patron_password_required';\r
+const SET_AUTO_RENEW_INTERVAL = 'circ.checkout_auto_renew_age';\r
+const SET_WORKSTATION_REQUIRED = 'circ.selfcheck.workstation_required';\r
+const SET_ALERT_POPUP = 'circ.selfcheck.alert.popup';\r
+const SET_ALERT_SOUND = 'circ.selfcheck.alert.sound';\r
+const SET_CC_PAYMENT_ALLOWED = 'credit.payments.allow';\r
+// This setting only comes into play if COPY_NOT_AVAILABLE is in the SET_AUTO_OVERRIDE_EVENTS list\r
+const SET_BLOCK_CHECKOUT_ON_COPY_STATUS = 'circ.selfcheck.block_checkout_on_copy_status';\r
+\r
+function SelfCheckManager() {\r
+       selfCheckMgr = this;\r
+       switchTo('step1');\r
+       \r
+       this.timer = null;\r
+    this.cgi = new openils.CGI();\r
+    this.staff = null; \r
+    this.workstation = null;\r
+    this.authtoken = null;\r
+\r
+    this.patron = null; \r
+    this.patronBarcodeRegex = null;\r
+\r
+    this.checkouts = [];\r
+    this.itemsOut = [];\r
+\r
+    // During renewals, keep track of the ID of the previous circulation. \r
+    // Previous circ is used for tracking failed renewals (for receipts).\r
+    this.prevCirc = null;\r
+\r
+    // current item barcode\r
+    this.itemBarcode = null; \r
+\r
+    // are we currently performing a renewal?\r
+    this.isRenewal = false; \r
+\r
+    // dict of org unit settings for "here"\r
+    this.orgSettings = {};\r
+\r
+    // Construct a mock checkout for debugging purposes\r
+    if(this.mockCheckouts = this.cgi.param('mock-circ')) {\r
+\r
+        this.mockCheckout = {\r
+            payload : {\r
+                record : new fieldmapper.mvr(),\r
+                copy : new fieldmapper.acp(),\r
+                circ : new fieldmapper.circ()\r
+            }\r
+        };\r
+\r
+        this.mockCheckout.payload.record.title('Jazz improvisation for guitar');\r
+        this.mockCheckout.payload.record.author('Wise, Les');\r
+        this.mockCheckout.payload.record.isbn('0634033565');\r
+        this.mockCheckout.payload.copy.barcode('123456789');\r
+        this.mockCheckout.payload.circ.renewal_remaining(1);\r
+        this.mockCheckout.payload.circ.parent_circ(1);\r
+        this.mockCheckout.payload.circ.due_date('2012-12-21');\r
+    }\r
+\r
+    this.initPrinter();\r
+}\r
+\r
+SelfCheckManager.prototype.keepMeLoggedIn = function() {\r
+       //alert(this.timer);\r
+       if(this.timer) try {clearTimeout(this.timer)} catch(e){}\r
+       this.timer = setTimeout('selfCheckMgr.logoutPatron();', TIMEOUT*1000);\r
+}\r
+\r
+/**\r
+ * Fetch the org-unit settings, initialize the display, etc.\r
+ */\r
+SelfCheckManager.prototype.init = function() {\r
+    this.staff = openils.User.user;\r
+    this.workstation = openils.User.workstation;\r
+    this.authtoken = openils.User.authtoken;\r
+    this.loadOrgSettings();\r
+\r
+    this.circTbody = dojo.byId('oils-selfck-circ-tbody');\r
+    this.itemsOutTbody = dojo.byId('oils-selfck-circ-out-tbody');\r
+\r
+    // workstation is required but none provided\r
+    if(this.orgSettings[SET_WORKSTATION_REQUIRED] && !this.workstation) {\r
+        if(confirm(dojo.string.substitute(localeStrings.WORKSTATION_REQUIRED))) {\r
+            this.registerWorkstation();\r
+        }\r
+        return;\r
+    }\r
+    \r
+    var self = this;\r
+    // connect onclick handlers to the various navigation links\r
+    var linkHandlers = {\r
+        'oils-selfck-hold-details-link' : function() { self.drawHoldsPage(true); },\r
+        'oils-selfck-view-fines-link' : function() { self.drawFinesPage(); openils.Util.show('oils-selfck-fines-tbody'); openils.Util.hide('pay_fines'); },\r
+        'oils-selfck-pay-fines-link' : function() {\r
+            switchTo('step3','step3c');\r
+                       openils.Util.hide('oils-selfck-fines-tbody');\r
+                       openils.Util.show('pay_fines');\r
+                       self.keepMeLoggedIn();\r
+            self.drawPayFinesPage(\r
+                self.patron,\r
+                self.getSelectedFinesTotal(),\r
+                self.getSelectedFineTransactions(),\r
+                function(resp) {\r
+                    var evt = openils.Event.parse(resp);\r
+                    if(evt) {\r
+                        var message = evt + '';\r
+                        if(evt.textcode == 'CREDIT_PROCESSOR_DECLINED_TRANSACTION' && evt.payload)\r
+                            message += '\n' + evt.payload.error_message;\r
+                        self.handleAlert(message, true, 'payment-failure');\r
+                        return;\r
+                    }\r
+                                       self.patron.last_xact_id(resp.last_xact_id);\r
+                    self.printPaymentReceipt(\r
+                        resp,\r
+                        function() {\r
+                            self.updateFinesSummary();\r
+                            self.drawFinesPage();\r
+                        }\r
+                    );\r
+                }\r
+            );\r
+        },\r
+        //'oils-selfck-nav-home' : function() { self.drawCircPage(); },\r
+        'oils-selfck-nav-logout' : function() { self.logoutPatron(); },\r
+        'oils-selfck-nav-logout-print' : function() { self.logoutPatron(true); },\r
+        'oils-selfck-items-out-details-link' : function() { self.drawItemsOutPage(); },\r
+        //'oils-selfck-print-list-link' : function() { self.printList(); }\r
+    }\r
+\r
+    for(var id in linkHandlers) {\r
+               //var obj1 = dojo.byId(id);\r
+               //obj1.onclick = linkHandlers[id];\r
+        dojo.connect(dojo.byId(id), 'onclick', linkHandlers[id]);\r
+       }\r
+\r
+\r
+    if(this.cgi.param('patron')) {\r
+        \r
+        // Patron barcode via cgi param.  Mainly used for debugging and\r
+        // only works if password is not required by policy\r
+        this.loginPatron(this.cgi.param('patron'));\r
+\r
+    } else {\r
+        this.drawLoginPage();\r
+    }\r
+\r
+    /**\r
+     * To test printing, pass a URL param of 'testprint'.  The value for the param\r
+     * should be a JSON string like so:  [{circ:<circ_id>}, ...]\r
+     */\r
+    var testPrint = this.cgi.param('testprint');\r
+    if(testPrint) {\r
+        this.checkouts = JSON2js(testPrint);\r
+        this.printSessionReceipt();\r
+        this.checkouts = [];\r
+    }\r
+}\r
+\r
+\r
+SelfCheckManager.prototype.getSelectedFinesTotal = function() {\r
+    var total = 0;\r
+    dojo.forEach(\r
+        dojo.query("[name=selector]", this.finesTbody),\r
+        function(input) {\r
+            if(input.checked)\r
+                total += Number(input.balance_owed);\r
+        }\r
+    );\r
+    return total.toFixed(2);\r
+};\r
+\r
+SelfCheckManager.prototype.getSelectedFineTransactions = function() {\r
+    return dojo.query("[name=selector]", this.finesTbody).\r
+        filter(function (o) { return o.checked }).\r
+        map(\r
+            function (o) {\r
+                return [\r
+                    o.getAttribute("xact"),\r
+                    Number(o.balance_owed).toFixed(2)\r
+                ];\r
+            }\r
+        );\r
+};\r
+\r
+/**\r
+ * Registers a new workstion\r
+ */\r
+SelfCheckManager.prototype.registerWorkstation = function() {\r
+    \r
+    oilsSelfckWsDialog.show();\r
+\r
+    new openils.User().buildPermOrgSelector(\r
+        'REGISTER_WORKSTATION', \r
+        oilsSelfckWsLocSelector, \r
+        this.staff.home_ou()\r
+    );\r
+\r
+\r
+    var self = this;\r
+    dojo.connect(oilsSelfckWsSubmit, 'onClick', \r
+\r
+        function() {\r
+            oilsSelfckWsDialog.hide();\r
+            var name = oilsSelfckWsLocSelector.attr('displayedValue') + '-' + oilsSelfckWsName.attr('value');\r
+\r
+            var res = fieldmapper.standardRequest(\r
+                ['open-ils.actor', 'open-ils.actor.workstation.register'],\r
+                { params : [\r
+                        self.authtoken, name, oilsSelfckWsLocSelector.attr('value')\r
+                    ]\r
+                }\r
+            );\r
+\r
+            if(evt = openils.Event.parse(res)) {\r
+                if(evt.textcode == 'WORKSTATION_NAME_EXISTS') {\r
+                    if(confirm(localeStrings.WORKSTATION_EXISTS)) {\r
+                        location.href = location.href.replace(/\?.*/, '') + '?ws=' + name;\r
+                    } else {\r
+                        self.registerWorkstation();\r
+                    }\r
+                    return;\r
+                } else {\r
+                    alert(evt);\r
+                }\r
+            } else {\r
+                location.href = location.href.replace(/\?.*/, '') + '?ws=' + name;\r
+            }\r
+        }\r
+    );\r
+}\r
+\r
+/**\r
+ * Loads the org unit settings\r
+ */\r
+SelfCheckManager.prototype.loadOrgSettings = function() {\r
+\r
+    var settings = fieldmapper.aou.fetchOrgSettingBatch(\r
+        this.staff.ws_ou(), [\r
+            SET_BARCODE_REGEX,\r
+            SET_PATRON_TIMEOUT,\r
+            SET_ALERT_POPUP,\r
+            SET_ALERT_SOUND,\r
+            SET_AUTO_OVERRIDE_EVENTS,\r
+            SET_BLOCK_CHECKOUT_ON_COPY_STATUS,\r
+            SET_PATRON_PASSWORD_REQUIRED,\r
+            SET_AUTO_RENEW_INTERVAL,\r
+            SET_WORKSTATION_REQUIRED,\r
+            SET_CC_PAYMENT_ALLOWED\r
+        ]\r
+    );\r
+\r
+    for(k in settings) {\r
+        if(settings[k])\r
+            this.orgSettings[k] = settings[k].value;\r
+    }\r
+\r
+    if(settings[SET_BARCODE_REGEX]) \r
+        this.patronBarcodeRegex = new RegExp(settings[SET_BARCODE_REGEX].value);\r
+}\r
+\r
+SelfCheckManager.prototype.drawLoginPage = function() {\r
+    var self = this;\r
+    var bcHandler = function(barcode) {\r
+        // handle patron barcode entry\r
+\r
+        if(self.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) {\r
+            \r
+            // password is required.  wire up the scan box to read it\r
+            self.updateScanBox({\r
+                msg : 'Please enter your password', // TODO i18n \r
+                handler : function(pw) { self.loginPatron(barcode, pw); },\r
+                password : true\r
+            });\r
+\r
+        } else {\r
+            // password is not required, go ahead and login\r
+            self.loginPatron(barcode);\r
+        }\r
+    };\r
+\r
+    this.updateScanBox({\r
+        msg : 'Please log in with your library barcode.', // TODO\r
+        handler : bcHandler\r
+    });\r
+       \r
+       var txtBox = (dojo.byId('step2').style.display=='none') ? 'patron-login-username' : 'patron-login-password';\r
+       try{var a=dojo.byId(txtBox);a.focus();a.select();}catch(e){}\r
+}\r
+\r
+/**\r
+ * Login the patron.  \r
+ */\r
+SelfCheckManager.prototype.loginPatron = function(barcode, passwd) {\r
+       \r
+    //if(this.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) { // password always reqired, per KCLS - fail safe\r
+        if(!passwd) {\r
+            // would only happen in dev/debug mode when using the patron= param\r
+            alert('password required by org setting.  remove patron= from URL'); \r
+            return;\r
+        }\r
+\r
+        // patron password is required.  Verify it.\r
+\r
+        var res = fieldmapper.standardRequest(\r
+            ['open-ils.actor', 'open-ils.actor.verify_user_password'],\r
+            {params : [this.authtoken, barcode, null, hex_md5(passwd)]}\r
+        );\r
+\r
+        if(res == 0) {\r
+            // user-not-found results in login failure\r
+            this.handleAlert(\r
+                dojo.string.substitute(localeStrings.LOGIN_FAILED, [barcode]),\r
+                false, 'login-failure'\r
+            );\r
+            this.drawLoginPage();\r
+                       openils.Util.show('back_to_login');\r
+            return;\r
+        }\r
+    //} \r
+\r
+    // retrieve the fleshed user by barcode\r
+    this.patron = fieldmapper.standardRequest(\r
+        ['open-ils.actor', 'open-ils.actor.user.fleshed.retrieve_by_barcode'],\r
+        {params : [this.authtoken, barcode]}\r
+    );\r
+\r
+    var evt = openils.Event.parse(this.patron);\r
+    if(evt) {\r
+        this.handleAlert(\r
+            dojo.string.substitute(localeStrings.LOGIN_FAILED, [barcode]),\r
+            false, 'login-failure'\r
+        );\r
+        this.drawLoginPage();\r
+               openils.Util.show('back_to_login');\r
+\r
+    } else {\r
+\r
+        //this.handleAlert('', true, 'login-success');\r
+        dojo.byId('user_name').innerHTML = \r
+            dojo.string.substitute(localeStrings.WELCOME_BANNER, [this.patron.first_given_name()]);\r
+               dojo.byId('oils-selfck-status-div').innerHTML = '';\r
+               dojo.byId('oils-selfck-status-div2').innerHTML = '';\r
+               dojo.byId('oils-selfck-status-div3').innerHTML = '';\r
+               openils.Util.hide('back_to_login');\r
+        this.drawCircPage();\r
+    }\r
+}\r
+\r
+\r
+SelfCheckManager.prototype.handleAlert = function(message, shouldPopup, sound) {\r
+    console.log("Handling alert " + message);\r
+\r
+    dojo.byId('oils-selfck-status-div').innerHTML = message;\r
+       if(!this.patron){\r
+               dojo.byId('oils-selfck-status-div2').innerHTML = message;\r
+               dojo.byId('oils-selfck-status-div3').innerHTML = message;\r
+       }\r
+       \r
+    if(shouldPopup && this.orgSettings[SET_ALERT_POPUP]) \r
+        alert(message);\r
+\r
+    if(this.orgSettings[SET_ALERT_SOUND])\r
+        openils.Util.playAudioUrl(SelfCheckManager.audioConfig[sound]);\r
+}\r
+\r
+\r
+/**\r
+ * Manages the main input box\r
+ * @param msg The context message to display with the box\r
+ * @param clearOnly Don't update the context message, just clear the value and re-focus\r
+ * @param handler Optional "on-enter" handler.  \r
+ */\r
+SelfCheckManager.prototype.updateScanBox = function(args) {\r
+    args = args || {};\r
+\r
+    if(args.select) {\r
+        selfckScanBox.domNode.select();\r
+    } else {\r
+        selfckScanBox.attr('value', '');\r
+    }\r
+\r
+    if(args.password) {\r
+        selfckScanBox.domNode.setAttribute('type', 'password');\r
+    } else {\r
+        selfckScanBox.domNode.setAttribute('type', '');\r
+    }\r
+\r
+    if(args.value)\r
+        selfckScanBox.attr('value', args.value);\r
+\r
+    if(args.msg) \r
+        dojo.byId('oils-selfck-scan-text').innerHTML = args.msg;\r
+\r
+    if(selfckScanBox._lastHandler && (args.handler || args.clearHandler)) {\r
+        dojo.disconnect(selfckScanBox._lastHandler);\r
+    }\r
+\r
+    if(args.handler) {\r
+\r
+        selfckScanBox._lastHandler = dojo.connect(\r
+            selfckScanBox, \r
+            'onKeyDown', \r
+            function(e) {\r
+                if(e.keyCode != dojo.keys.ENTER) \r
+                    return;\r
+                args.handler(selfckScanBox.attr('value'));\r
+            }\r
+        );\r
+    }\r
+\r
+    selfckScanBox.focus();\r
+}\r
+\r
+/**\r
+ *  Sets up the checkout/renewal interface\r
+ */\r
+SelfCheckManager.prototype.drawCircPage = function() {\r
+       this.keepMeLoggedIn();\r
+    openils.Util.show('oils-selfck-circ-tbody', 'table-row-group');\r
+       switchTo('step3');\r
+\r
+    var self = this;\r
+    this.updateScanBox({\r
+        msg : 'Please enter an item barcode', // TODO i18n\r
+        handler : function(barcode) { \r
+               openils.Util.show('oils-selfck-fines-tbody'); \r
+               openils.Util.hide('pay_fines'); switchTo('step3'); \r
+               self.checkout(barcode); }\r
+    });\r
+\r
+    if(!this.circTemplate)\r
+        this.circTemplate = this.circTbody.removeChild(dojo.byId('oils-selfck-circ-row'));\r
+\r
+    // fines summary\r
+    this.updateFinesSummary();\r
+\r
+    // holds summary\r
+    this.updateHoldsSummary();\r
+\r
+    // items out summary\r
+    this.updateCircSummary();\r
+\r
+    // render mock checkouts for debugging?\r
+    if(this.mockCheckouts) {\r
+        for(var i in [1,2,3]) \r
+            this.displayCheckout(this.mockCheckout, 'checkout');\r
+    }\r
+}\r
+\r
+\r
+SelfCheckManager.prototype.updateFinesSummary = function() {\r
+    var self = this; \r
+\r
+    // fines summary\r
+    fieldmapper.standardRequest(\r
+        ['open-ils.actor', 'open-ils.actor.user.fines.summary'],\r
+        {   async : true,\r
+            params : [this.authtoken, this.patron.id()],\r
+            oncomplete : function(r) {\r
+                var summary = openils.Util.readResponse(r);\r
+                               var finesSum = dojo.byId('acct_fines');\r
+                               var bal = summary.balance_owed();\r
+                               var bal2 = parseFloat(bal);\r
+                               \r
+                               if(bal2>0) {finesSum.style.color="red"; openils.Util.show('oils-selfck-pay-fines-link');}\r
+                finesSum.innerHTML = dojo.string.substitute(localeStrings.TOTAL_FINES_ACCOUNT, [bal2.toFixed(2)]);\r
+                self.creditPayableBalance = bal2+'';\r
+            }\r
+        }\r
+    );\r
+}\r
+\r
+\r
+SelfCheckManager.prototype.drawItemsOutPage = function() {\r
+       this.keepMeLoggedIn();\r
+       switchTo('step3','step3d');\r
+\r
+       if(!this.outTemplate)\r
+        this.outTemplate = this.itemsOutTbody.removeChild(dojo.byId('oils-selfck-circ-out-row'));\r
+    while(this.itemsOutTbody.childNodes[0])\r
+        this.itemsOutTbody.removeChild(this.itemsOutTbody.childNodes[0]);\r
+\r
+    progressDialog.show(true);\r
+    var self = this;\r
+       \r
+    fieldmapper.standardRequest(\r
+        ['open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic'],\r
+        {\r
+            async : true,\r
+            params : [this.authtoken, this.patron.id()],\r
+            oncomplete : function(r) {\r
+                var resp = openils.Util.readResponse(r);\r
+\r
+                var circs = resp.sort(\r
+                    function(a, b) {\r
+                        if(a.circ.due_date() > b.circ.due_date())\r
+                            return -1;\r
+                        return 1;\r
+                    }\r
+                );\r
+\r
+                self.itemsOut = [];\r
+                dojo.forEach(circs,\r
+                    function(circ) {\r
+                        self.itemsOut.push(circ.circ.id());\r
+                        handleCheckedItems(circ);\r
+                    }\r
+                );\r
+                               progressDialog.hide();\r
+            }\r
+        }\r
+    );\r
+}\r
+\r
+function handleCheckedItems(circ) {\r
+       var self = selfCheckMgr;\r
+       var row = self.outTemplate.cloneNode(true);\r
+       \r
+       self.byName(row,'barcode').innerHTML = circ.copy.barcode();\r
+       self.byName(row,'title').innerHTML = circ.record.title();\r
+       self.byName(row,'author').innerHTML = circ.record.author();\r
+       if(dojo.date.stamp.fromISOString(circ.circ.due_date())<(new Date())) self.byName(row,'due_date').style.color="red";\r
+       self.byName(row,'due_date').innerHTML = dojo.date.locale.format(dojo.date.stamp.fromISOString(circ.circ.due_date()), {selector: 'date', fullYear: true});\r
+       self.byName(row,'format').innerHTML = circ.record.types_of_resource()[0];\r
+       \r
+       self.itemsOutTbody.appendChild(row);\r
+}\r
+\r
+SelfCheckManager.prototype.goToTab = function(name) {\r
+    this.tabName = name;\r
+\r
+    openils.Util.hide('oils-selfck-fines-page');\r
+    openils.Util.hide('oils-selfck-payment-page');\r
+    openils.Util.hide('oils-selfck-holds-page');\r
+    openils.Util.hide('oils-selfck-circ-page');\r
+    openils.Util.hide('oils-selfck-pay-fines-link');\r
+    \r
+    switch(name) {\r
+        case 'checkout':\r
+            openils.Util.show('oils-selfck-circ-page');\r
+            break;\r
+        case 'items_out':\r
+            openils.Util.show('oils-selfck-circ-page');\r
+            break;\r
+        case 'holds':\r
+            openils.Util.show('oils-selfck-holds-page');\r
+            break;\r
+        case 'fines':\r
+            openils.Util.show('oils-selfck-fines-page');\r
+            break;\r
+        case 'payment':\r
+            openils.Util.show('oils-selfck-payment-page');\r
+            break;\r
+    }\r
+}\r
+\r
+\r
+SelfCheckManager.prototype.printList = function(which) {\r
+       this.keepMeLoggedIn();\r
+    switch(which) {\r
+        case 'checkout':\r
+            this.printSessionReceipt();\r
+            break;\r
+        case 'items_out':\r
+            this.printItemsOutReceipt();\r
+            break;\r
+        case 'holds':\r
+            this.printHoldsReceipt();\r
+            break;\r
+        case 'fines':\r
+            this.printFinesReceipt();\r
+            break;\r
+    }\r
+}\r
+\r
+SelfCheckManager.prototype.updateHoldsSummary = function() {\r
+    if(!this.holdsSummary) {\r
+        var summary = fieldmapper.standardRequest(\r
+            ['open-ils.circ', 'open-ils.circ.holds.user_summary'],\r
+            {params : [this.authtoken, this.patron.id()]}\r
+        );\r
+\r
+        this.holdsSummary = {};\r
+        this.holdsSummary.ready = Number(summary['4']);\r
+        this.holdsSummary.total = 0;\r
+\r
+        for(var i in summary)\r
+            this.holdsSummary.total += Number(summary[i]);\r
+    }\r
+\r
+    dojo.byId('oils-selfck-holds-total').innerHTML =dojo.string.substitute("${0}) Item"+(this.holdsSummary.total==1?"":"s"),[this.holdsSummary.total]);\r
+    dojo.byId('oils-selfck-holds-ready').innerHTML =dojo.string.substitute("${0}) Item"+(this.holdsSummary.ready==1?"":"s"),[this.holdsSummary.ready]);\r
+}\r
+\r
+\r
+SelfCheckManager.prototype.updateCircSummary = function(increment) {\r
+    if(!this.circSummary) {\r
+\r
+        var summary = fieldmapper.standardRequest(\r
+            ['open-ils.actor', 'open-ils.actor.user.checked_out.count'],\r
+            {params : [this.authtoken, this.patron.id()]}\r
+        );\r
+\r
+        this.circSummary = {\r
+            total : Number(summary.out) + Number(summary.overdue),\r
+            overdue : Number(summary.overdue),\r
+            session : 0\r
+        };\r
+    }\r
+\r
+    if(increment) {\r
+        // local checkout occurred.  Add to the total and the session.\r
+        this.circSummary.total += 1;\r
+        this.circSummary.session += 1;\r
+    }\r
+\r
+    dojo.byId('oils-selfck-circ-account-total').innerHTML = dojo.string.substitute("${0}) Item"+(this.circSummary.total==1?"":"s"), [this.circSummary.total]);\r
+\r
+    /*\r
+       dojo.byId('oils-selfck-circ-session-total').innerHTML = \r
+        dojo.string.substitute(\r
+            localeStrings.TOTAL_ITEMS_SESSION, \r
+            [this.circSummary.session]\r
+        );\r
+       */\r
+}\r
+\r
+\r
+SelfCheckManager.prototype.drawHoldsPage = function(bool) {\r
+       this.keepMeLoggedIn();\r
+       if(bool) switchTo('step3','step3f'); else switchTo('step3','step3e');\r
+\r
+    this.holdTbody = dojo.byId('oils-selfck-hold-tbody');\r
+       this.readyTbody = dojo.byId('oils-selfck-rdy-tbody');\r
+       if(!this.readyTemplate)\r
+        this.readyTemplate = this.readyTbody.removeChild(dojo.byId('oils-selfck-rdy-row'));\r
+    if(!this.holdTemplate)\r
+        this.holdTemplate = this.holdTbody.removeChild(dojo.byId('oils-selfck-hold-row'));\r
+    while(this.holdTbody.childNodes[0])\r
+        this.holdTbody.removeChild(this.holdTbody.childNodes[0]);\r
+       while(this.readyTbody.childNodes[0])\r
+        this.readyTbody.removeChild(this.readyTbody.childNodes[0]);\r
+\r
+    progressDialog.show(true);\r
+\r
+    var self = this;\r
+    fieldmapper.standardRequest( // fetch the hold IDs\r
+\r
+        ['open-ils.circ', 'open-ils.circ.holds.id_list.retrieve'],\r
+        {   async : true,\r
+            params : [this.authtoken, this.patron.id()],\r
+\r
+            oncomplete : function(r) { \r
+                var ids = openils.Util.readResponse(r);\r
+                if(!ids || ids.length == 0) {\r
+                    progressDialog.hide();\r
+                    return;\r
+                }\r
+\r
+                fieldmapper.standardRequest( // fetch the hold objects with fleshed details\r
+                    ['open-ils.circ', 'open-ils.circ.hold.details.batch.retrieve'],\r
+                    {   async : true,\r
+                        params : [self.authtoken, ids],\r
+\r
+                        onresponse : function(rr) {\r
+                                                       progressDialog.hide(); \r
+                            self.drawHolds(openils.Util.readResponse(rr));\r
+                        }\r
+                    }\r
+                );\r
+            }\r
+        }\r
+    );\r
+}\r
+\r
+/**\r
+ * Fetch and add a single hold to the list of holds\r
+ */\r
+SelfCheckManager.prototype.drawHolds = function(holds) {\r
+       //this.keepMeLoggedIn();\r
+    this.holds = holds;\r
+    progressDialog.hide();\r
+       \r
+       var data = holds;\r
+       if(!data) return;\r
+       var row = this.holdTemplate.cloneNode(true);\r
+       var row2 = this.readyTemplate.cloneNode(true);\r
+\r
+       //if(data.mvr.isbn()) {\r
+       //    this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + data.mvr.isbn());\r
+       //}\r
+       \r
+       if(data.status == 4) {\r
+               this.byName(row2, 'title').innerHTML = data.mvr.title();\r
+               this.byName(row2, 'format').innerHTML = data.mvr.types_of_resource()[0];\r
+               this.byName(row2, 'lib').innerHTML = fieldmapper.aou.findOrgUnit(data.hold.pickup_lib()).name();\r
+               if(dojo.date.stamp.fromISOString(data.hold.capture_time())<(new Date())) this.byName(row2, 'date').style.color="red";\r
+               this.byName(row2, 'date').innerHTML = dojo.date.locale.format(dojo.date.stamp.fromISOString(data.hold.capture_time()), {selector: 'date', fullYear: true});\r
+               this.readyTbody.appendChild(row2);\r
+       } else {\r
+\r
+               this.byName(row, 'title').innerHTML = data.mvr.title();\r
+               this.byName(row, 'author').innerHTML = data.mvr.author();\r
+               this.byName(row, 'format').innerHTML = data.mvr.types_of_resource()[0];\r
+\r
+                       // hold is still pending\r
+               this.byName(row, 'status').innerHTML = dojo.string.substitute(localeStrings.HOLD_STATUS_WAITING,[data.queue_position, data.potential_copies]);\r
+               this.holdTbody.appendChild(row);\r
+       }\r
+}\r
+\r
+\r
+SelfCheckManager.prototype.drawFinesPage = function() {\r
+       this.keepMeLoggedIn();\r
+    // TODO add option to hid scanBox\r
+    // this.updateScanBox(...)\r
+\r
+    //this.goToTab('fines');\r
+       switchTo('step3','step3c');\r
+    progressDialog.show(true);\r
+\r
+    //if(this.creditPayableBalance > 0 && this.orgSettings[SET_CC_PAYMENT_ALLOWED])\r
+    //  openils.Util.show('oils-selfck-pay-fines-link', 'inline');\r
+    \r
+\r
+    this.finesTbody = dojo.byId('oils-selfck-fines-tbody');\r
+    if(!this.finesTemplate)\r
+        this.finesTemplate = this.finesTbody.removeChild(dojo.byId('oils-selfck-fines-row'));\r
+    while(this.finesTbody.childNodes[0])\r
+        this.finesTbody.removeChild(this.finesTbody.childNodes[0]);\r
+\r
+/*\r
+    // when user clicks on a selector checkbox, update the total owed\r
+    var updateSelected = function() {\r
+        var total = 0;\r
+        dojo.forEach(\r
+            dojo.query('[name=selector]', this.finesTbody),\r
+            function(input) {\r
+                if(input.checked)\r
+                    total += Number(input.getAttribute('balance_owed'));\r
+            }\r
+        );\r
+\r
+        total = total.toFixed(2);\r
+        dojo.byId('oils-selfck-selected-total').innerHTML = \r
+            dojo.string.substitute(localeStrings.TOTAL_FINES_SELECTED, [total]);\r
+    }\r
+\r
+    // wire up the batch on/off selector\r
+    var sel = dojo.byId('oils-selfck-fines-selector');\r
+    sel.onchange = function() {\r
+        dojo.forEach(\r
+            dojo.query('[name=selector]', this.finesTbody),\r
+            function(input) {\r
+                input.checked = sel.checked;\r
+            }\r
+        );\r
+    };\r
+*/\r
+    var self = this;\r
+    var handler = function(dataList) {\r
+\r
+        self.finesCount = dataList.length;\r
+        self.finesData = dataList;\r
+\r
+        for(var i in dataList) {\r
+\r
+            var data = dataList[i];\r
+            var row = self.finesTemplate.cloneNode(true);\r
+            var type = data.transaction.xact_type();\r
+\r
+            if(type == 'circulation') {\r
+                self.byName(row, 'title').innerHTML = data.record.title();\r
+                               if(dojo.date.stamp.fromISOString(data.circ.due_date())<(new Date())) self.byName(row, 'due_date').style.color="red";\r
+                self.byName(row, 'due_date').innerHTML = dojo.date.locale.format(dojo.date.stamp.fromISOString(data.circ.due_date()), {selector: 'date', fullYear: true});\r
+                               self.byName(row, 'date_return').innerHTML = (data.circ.checkin_time())?dojo.date.locale.format(dojo.date.stamp.fromISOString(data.circ.checkin_time()), {selector: 'date', fullYear: true}):"";\r
+\r
+            } else if(type == 'grocery') {\r
+                self.byName(row, 'title').innerHTML = (data.transaction.last_billing_type())?("Miscellaneous - "+data.transaction.last_billing_type()):"Miscellaneous"; // Go ahead and head off any confusion around "grocery".  TODO i18n\r
+            }\r
+\r
+            //self.byName(row, 'total_owed').innerHTML = data.transaction.total_owed();\r
+            //self.byName(row, 'total_paid').innerHTML = data.transaction.total_paid();\r
+            self.byName(row, 'balance').innerHTML = data.transaction.balance_owed();\r
+                       self.byName(row, 'selector').balance_owed = data.transaction.balance_owed();\r
+                       self.byName(row, 'selector').setAttribute('xact', data.transaction.id());\r
+/*\r
+            // row selector\r
+            var selector = self.byName(row, 'selector')\r
+            selector.onchange = updateSelected;\r
+            selector.setAttribute('xact', data.transaction.id());\r
+            selector.setAttribute('balance_owed', data.transaction.balance_owed());\r
+            selector.checked = true;\r
+*/\r
+            self.finesTbody.appendChild(row);\r
+        }\r
+\r
+        //updateSelected();\r
+    }\r
+\r
+\r
+    fieldmapper.standardRequest( \r
+        ['open-ils.actor', 'open-ils.actor.user.transactions.have_balance.fleshed'],\r
+        {   async : true,\r
+            params : [this.authtoken, this.patron.id()],\r
+            oncomplete : function(r) { \r
+                progressDialog.hide();\r
+                handler(openils.Util.readResponse(r));\r
+            }\r
+        }\r
+    );\r
+}\r
+\r
+SelfCheckManager.prototype.checkin = function(barcode, abortTransit) {\r
+    var resp = fieldmapper.standardRequest(\r
+        ['open-ils.circ', 'open-ils.circ.transit.abort'],\r
+        {params : [this.authtoken, {barcode : barcode}]}\r
+    );\r
+\r
+    // resp == 1 on success\r
+    if(openils.Event.parse(resp))\r
+        return false;\r
+\r
+    var resp = fieldmapper.standardRequest(\r
+        ['open-ils.circ', 'open-ils.circ.checkin.override'],\r
+        {params : [\r
+            this.authtoken, {\r
+                patron_id : this.patron.id(),\r
+                copy_barcode : barcode,\r
+                noop : true\r
+            }\r
+        ]}\r
+    );\r
+\r
+    if(!resp.length) resp = [resp];\r
+    for(var i = 0; i < resp.length; i++) {\r
+        var tc = openils.Event.parse(resp[i]).textcode;\r
+        if(tc == 'SUCCESS' || tc == 'NO_CHANGE') {\r
+            continue;\r
+        } else {\r
+            return false;\r
+        }\r
+    }\r
+\r
+    return true;\r
+}\r
+\r
+/**\r
+ * Check out a single item.  If the item is already checked \r
+ * out to the patron, redirect to renew()\r
+ */\r
+SelfCheckManager.prototype.checkout = function(barcode, override) {\r
+       this.keepMeLoggedIn();\r
+    this.prevCirc = null;\r
+\r
+    if(!barcode) {\r
+        this.updateScanbox(null, true);\r
+        return;\r
+    }\r
+\r
+    if(this.mockCheckouts) {\r
+        // if we're in mock-checkout mode, just insert another\r
+        // fake circ into the table and get out of here.\r
+        this.displayCheckout(this.mockCheckout, 'checkout');\r
+        return;\r
+    }\r
+\r
+    // TODO see if it's a patron barcode\r
+    // TODO see if this item has already been checked out in this session\r
+\r
+    var method = 'open-ils.circ.checkout.full';\r
+    if(override) method += '.override';\r
+\r
+    console.log("Checkout out item " + barcode + " with method " + method);\r
+\r
+    var result = fieldmapper.standardRequest(\r
+        ['open-ils.circ', method],\r
+        {params: [\r
+            this.authtoken, {\r
+                patron_id : this.patron.id(),\r
+                copy_barcode : barcode\r
+            }\r
+        ]}\r
+    );\r
+\r
+    var stat = this.handleXactResult('checkout', barcode, result);\r
+\r
+    if(stat.override) {\r
+        this.checkout(barcode, true);\r
+    } else if(stat.doOver) {\r
+        this.checkout(barcode);\r
+    } else if(stat.renew) {\r
+        this.renew(barcode);\r
+    }\r
+}\r
+\r
+SelfCheckManager.prototype.failPartMessage = function(result) {\r
+    if (result.payload && result.payload.fail_part) {\r
+        var stringKey = "FAIL_PART_" +\r
+            result.payload.fail_part.replace(/\./g, "_");\r
+        return localeStrings[stringKey];\r
+    } else {\r
+        return null;\r
+    }\r
+}\r
+\r
+SelfCheckManager.prototype.handleXactResult = function(action, item, result) {\r
+    var displayText = '';\r
+\r
+    // If true, the display message is important enough to pop up.  Whether or not\r
+    // an alert() actually occurs, depends on org unit settings\r
+    var popup = false;  \r
+    var sound = ''; // sound file reference\r
+    var payload = result.payload || {};\r
+    var overrideEvents = this.orgSettings[SET_AUTO_OVERRIDE_EVENTS];\r
+    var blockStatuses = this.orgSettings[SET_BLOCK_CHECKOUT_ON_COPY_STATUS];\r
+       result.payload = payload;\r
+        \r
+    if(result.textcode == 'NO_SESSION') {\r
+\r
+        return this.logoutStaff();\r
+\r
+    } else if(result.textcode == 'SUCCESS') {\r
+\r
+        if(action == 'checkout') {\r
+\r
+            displayText = dojo.string.substitute(localeStrings.CHECKOUT_SUCCESS, [item]);\r
+            this.displayCheckout(result, 'checkout');\r
+\r
+            if(payload.holds_fulfilled && payload.holds_fulfilled.length) {\r
+                // A hold was fulfilled, update the hold numbers in the circ summary\r
+                console.log("fulfilled hold " + payload.holds_fulfilled + " during checkout");\r
+                this.holdsSummary = null;\r
+                this.updateHoldsSummary();\r
+            }\r
+\r
+            this.updateCircSummary(true);\r
+\r
+        } else if(action == 'renew') {\r
+\r
+            displayText = dojo.string.substitute(localeStrings.RENEW_SUCCESS, [item]);\r
+            this.displayCheckout(result, 'renew');\r
+        }\r
+\r
+        this.checkouts.push({circ : result.payload.circ.id()});\r
+        sound = 'checkout-success';\r
+        this.updateScanBox();\r
+\r
+    } else if(result.textcode == 'OPEN_CIRCULATION_EXISTS' && action == 'checkout') {\r
+\r
+        // Server says the item is already checked out.  If it's checked out to the\r
+        // current user, we may need to renew it.  \r
+\r
+        if(payload.old_circ) { \r
+\r
+            /*\r
+            old_circ refers to the previous checkout IFF it's for the same user. \r
+            If no auto-renew interval is not defined, assume we should renew it\r
+            If an auto-renew interval is defined and the payload comes back with\r
+            auto_renew set to true, do the renewal.  Otherwise, let the patron know\r
+            the item is already checked out to them.  */\r
+\r
+            if( !this.orgSettings[SET_AUTO_RENEW_INTERVAL] ||\r
+                (this.orgSettings[SET_AUTO_RENEW_INTERVAL] && payload.auto_renew) ) {\r
+                this.prevCirc = payload.old_circ.id();\r
+                return { renew : true };\r
+            }\r
+\r
+            popup = false;\r
+            sound = 'checkout-failure';\r
+            displayText = dojo.string.substitute(localeStrings.ALREADY_OUT, [item]);\r
+\r
+        } else {\r
+\r
+            if( // copy is marked lost.  if configured to do so, check it in and try again.\r
+                result.payload.copy && \r
+                result.payload.copy.status() == /* LOST */ 3 &&\r
+                overrideEvents && overrideEvents.length &&\r
+                overrideEvents.indexOf('COPY_STATUS_LOST') != -1) {\r
+\r
+                    if(this.checkin(item)) {\r
+                        return { doOver : true };\r
+                    }\r
+            }\r
+\r
+            \r
+            // item is checked out to some other user\r
+            popup = false;\r
+            sound = 'checkout-failure';\r
+            displayText = dojo.string.substitute(localeStrings.OPEN_CIRCULATION_EXISTS, [item]);\r
+        }\r
+\r
+        this.updateScanBox();\r
+\r
+    } else {\r
+\r
+    \r
+        if(overrideEvents && overrideEvents.length) {\r
+            \r
+            // see if the events we received are all in the list of\r
+            // events to override\r
+    \r
+            if(!result.length) result = [result];\r
+    \r
+            var override = true;\r
+            for(var i = 0; i < result.length; i++) {\r
+\r
+                var match = overrideEvents.filter(function(e) { return (e == result[i].textcode); })[0];\r
+\r
+                if(!match) {\r
+                    override = false;\r
+                    break;\r
+                }\r
+\r
+                if(result[i].textcode == 'COPY_NOT_AVAILABLE' && blockStatuses && blockStatuses.length) {\r
+\r
+                    var stat = result[i].payload.status(); // copy status\r
+                    if(typeof stat == 'object') stat = stat.id();\r
+\r
+                    var match2 = blockStatuses.filter(function(e) { return (e == stat); })[0];\r
+\r
+                    if(match2) { // copy is in a blocked status\r
+                        override = false;\r
+                        break;\r
+                    }\r
+                }\r
+\r
+                if(result[i].textcode == 'COPY_IN_TRANSIT') {\r
+                    // to override a transit, we have to abort the transit and check it in first\r
+                    if(this.checkin(item, true)) {\r
+                        return { doOver : true };\r
+                    } else {\r
+                        override = false;\r
+                    }\r
+                }\r
+            }\r
+\r
+            if(override) \r
+                return { override : true };\r
+        }\r
+    \r
+        this.updateScanBox();\r
+        popup = false;\r
+        sound = 'checkout-failure';\r
+\r
+        if(action == 'renew')\r
+            this.checkouts.push({circ : this.prevCirc, renewal_failure : true});\r
+\r
+        if(result.length) \r
+            result = result[0];\r
+\r
+        switch(result.textcode) {\r
+\r
+            // TODO custom handler for blocking penalties\r
+\r
+            case 'MAX_RENEWALS_REACHED' :\r
+                displayText = dojo.string.substitute(\r
+                    localeStrings.MAX_RENEWALS, [item]);\r
+                break;\r
+\r
+            case 'ITEM_NOT_CATALOGED' :\r
+                displayText = dojo.string.substitute(\r
+                    localeStrings.ITEM_NOT_CATALOGED, [item]);\r
+                break;\r
+\r
+            case 'OPEN_CIRCULATION_EXISTS' :\r
+                displayText = dojo.string.substitute(\r
+                    localeStrings.OPEN_CIRCULATION_EXISTS, [item]);\r
+\r
+                break;\r
+\r
+            default:\r
+                console.error('Unhandled event ' + result.textcode);\r
+\r
+                if (!(displayText = this.failPartMessage(result))) {\r
+                    if (action == 'checkout' || action == 'renew') {\r
+                        displayText = dojo.string.substitute(\r
+                            localeStrings.GENERIC_CIRC_FAILURE, [item]);\r
+                    } else {\r
+                        displayText = dojo.string.substitute(\r
+                            localeStrings.UNKNOWN_ERROR, [result.textcode]);\r
+                    }\r
+                }\r
+        }\r
+    }\r
+\r
+    this.handleAlert(displayText, popup, sound);\r
+    return {};\r
+}\r
+\r
+\r
+/**\r
+ * Renew an item\r
+ */\r
+SelfCheckManager.prototype.renew = function(barcode, override) {\r
+\r
+    var method = 'open-ils.circ.renew';\r
+    if(override) method += '.override';\r
+\r
+    console.log("Renewing item " + barcode + " with method " + method);\r
+\r
+    var result = fieldmapper.standardRequest(\r
+        ['open-ils.circ', method],\r
+        {params: [\r
+            this.authtoken, {\r
+                patron_id : this.patron.id(),\r
+                copy_barcode : barcode\r
+            }\r
+        ]}\r
+    );\r
+\r
+    console.log(js2JSON(result));\r
+\r
+    var stat = this.handleXactResult('renew', barcode, result);\r
+\r
+    if(stat.override)\r
+        this.renew(barcode, true);\r
+}\r
+\r
+/**\r
+ * Display the result of a checkout or renewal in the items out table\r
+ */\r
+SelfCheckManager.prototype.displayCheckout = function(evt, type, itemsOut) {\r
+    var copy = evt.payload.copy;\r
+    var record = evt.payload.record;\r
+    var circ = evt.payload.circ;\r
+    var row = this.circTemplate.cloneNode(true);\r
+\r
+    //if(record.isbn()) {\r
+    //    this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + record.isbn());\r
+    //}\r
+\r
+    this.byName(row, 'barcode').innerHTML = copy.barcode();\r
+    this.byName(row, 'title').innerHTML = record.title();\r
+    //this.byName(row, 'author').innerHTML = record.author();\r
+    //this.byName(row, 'remaining').innerHTML = circ.renewal_remaining();\r
+    openils.Util.show(this.byName(row, type));\r
+\r
+    var date = dojo.date.stamp.fromISOString(circ.due_date());\r
+    this.byName(row, 'due_date').innerHTML = \r
+        dojo.date.locale.format(date, {selector : 'date'});\r
+\r
+    // put new circs at the top of the list\r
+    var tbody = this.circTbody;\r
+    if(itemsOut) tbody = this.itemsOutTbody;\r
+    tbody.insertBefore(row, tbody.getElementsByTagName('tr')[0]);\r
+}\r
+\r
+\r
+SelfCheckManager.prototype.byName = function(node, name) {\r
+    return dojo.query('[name=' + name+']', node)[0];\r
+}\r
+\r
+\r
+SelfCheckManager.prototype.initPrinter = function() {\r
+    try { // Mozilla only\r
+               netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");\r
+        netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');\r
+        netscape.security.PrivilegeManager.enablePrivilege('UniversalPreferencesRead');\r
+        netscape.security.PrivilegeManager.enablePrivilege('UniversalPreferencesWrite');\r
+        var pref = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);\r
+        if (pref)\r
+            pref.setBoolPref('print.always_print_silent', true);\r
+    } catch(E) {\r
+        console.log("Unable to initialize auto-printing"); \r
+    }\r
+}\r
+\r
+/**\r
+ * Print a receipt for this session's checkouts\r
+ */\r
+SelfCheckManager.prototype.printSessionReceipt = function(callback) {\r
+    var circIds = [];\r
+    var circCtx = []; // circ context data.  in this case, renewal_failure info\r
+\r
+    // collect the circs and failure info\r
+    dojo.forEach(\r
+        this.checkouts, \r
+        function(blob) {\r
+            circIds.push(blob.circ);\r
+            circCtx.push({renewal_failure:blob.renewal_failure});\r
+        }\r
+    );\r
+\r
+    var params = [\r
+        this.authtoken, \r
+        this.staff.ws_ou(),\r
+        null,\r
+        'format.selfcheck.checkout',\r
+        'print-on-demand',\r
+        circIds,\r
+        circCtx\r
+    ];\r
+\r
+    var self = this;\r
+    fieldmapper.standardRequest(\r
+        ['open-ils.circ', 'open-ils.circ.fire_circ_trigger_events'],\r
+        {   \r
+            async : true,\r
+            params : params,\r
+            oncomplete : function(r) {\r
+                var resp = openils.Util.readResponse(r);\r
+                var output = resp.template_output();\r
+                if(output) {\r
+                    self.printData(output.data(), self.checkouts.length, callback); \r
+                } else {\r
+                    var error = resp.error_output();\r
+                    if(error) {\r
+                        throw new Error("Error creating receipt: " + error.data());\r
+                    } else {\r
+                        throw new Error("No receipt data returned from server");\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    );\r
+}\r
+\r
+SelfCheckManager.prototype.printData = function(data, numItems, callback) {\r
+    var win = window.open('', '', 'resizable,width=350,height=250,scrollbars=1'); \r
+    win.document.body.innerHTML = data;\r
+    win.print();\r
+\r
+    /*\r
+     * There is no way to know when the browser is done printing.\r
+     * Make a best guess at when to close the print window by basing\r
+     * the setTimeout wait on the number of items to be printed plus\r
+     * a small buffer\r
+     */\r
+    var sleepTime = 1000;\r
+    if(numItems > 0) \r
+        sleepTime += (numItems / 2) * 1000;\r
+\r
+    setTimeout(\r
+        function() { \r
+            win.close(); // close the print window\r
+            if(callback) callback(); // fire optional post-print callback\r
+        },\r
+        sleepTime \r
+    );\r
+}\r
+\r
+\r
+/**\r
+ * Print a receipt for this user's items out\r
+ */\r
+SelfCheckManager.prototype.printItemsOutReceipt = function(callback) {\r
+    if(!this.itemsOut.length) return;\r
+\r
+    progressDialog.show(true);\r
+\r
+    var params = [\r
+        this.authtoken, \r
+        this.staff.ws_ou(),\r
+        null,\r
+        'format.selfcheck.items_out',\r
+        'print-on-demand',\r
+        this.itemsOut\r
+    ];\r
+\r
+    var self = this;\r
+    fieldmapper.standardRequest(\r
+        ['open-ils.circ', 'open-ils.circ.fire_circ_trigger_events'],\r
+        {   \r
+            async : true,\r
+            params : params,\r
+            oncomplete : function(r) {\r
+                progressDialog.hide();\r
+                var resp = openils.Util.readResponse(r);\r
+                var output = resp.template_output();\r
+                if(output) {\r
+                    self.printData(output.data(), self.itemsOut.length, callback); \r
+                } else {\r
+                    var error = resp.error_output();\r
+                    if(error) {\r
+                        throw new Error("Error creating receipt: " + error.data());\r
+                    } else {\r
+                        throw new Error("No receipt data returned from server");\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    );\r
+}\r
+\r
+/**\r
+ * Print a receipt for this user's items out\r
+ */\r
+SelfCheckManager.prototype.printHoldsReceipt = function(callback) {\r
+    if(!this.holds.length) return;\r
+\r
+    progressDialog.show(true);\r
+\r
+    var holdIds = [];\r
+    var holdData = [];\r
+\r
+    dojo.forEach(this.holds,\r
+        function(data) {\r
+            holdIds.push(data.hold.id());\r
+            if(data.status == 4) {\r
+                holdData.push({ready : true});\r
+            } else {\r
+                holdData.push({\r
+                    queue_position : data.queue_position, \r
+                    potential_copies : data.potential_copies\r
+                });\r
+            }\r
+        }\r
+    );\r
+\r
+    var params = [\r
+        this.authtoken, \r
+        this.staff.ws_ou(),\r
+        null,\r
+        'format.selfcheck.holds',\r
+        'print-on-demand',\r
+        holdIds,\r
+        holdData\r
+    ];\r
+\r
+    var self = this;\r
+    fieldmapper.standardRequest(\r
+        ['open-ils.circ', 'open-ils.circ.fire_hold_trigger_events'],\r
+        {   \r
+            async : true,\r
+            params : params,\r
+            oncomplete : function(r) {\r
+                progressDialog.hide();\r
+                var resp = openils.Util.readResponse(r);\r
+                var output = resp.template_output();\r
+                if(output) {\r
+                    self.printData(output.data(), self.holds.length, callback); \r
+                } else {\r
+                    var error = resp.error_output();\r
+                    if(error) {\r
+                        throw new Error("Error creating receipt: " + error.data());\r
+                    } else {\r
+                        throw new Error("No receipt data returned from server");\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    );\r
+}\r
+\r
+\r
+SelfCheckManager.prototype.printPaymentReceipt = function(paymentIds, callback) {\r
+    var self = this;\r
+    progressDialog.show(true);\r
+\r
+    fieldmapper.standardRequest(\r
+        ['open-ils.circ', 'open-ils.circ.money.payment_receipt.print'],\r
+        {\r
+            async : true,\r
+            params : [this.authtoken, paymentIds],\r
+            oncomplete : function(r) {\r
+                var resp = openils.Util.readResponse(r);\r
+                var output = resp.template_output();\r
+                progressDialog.hide();\r
+                if(output) {\r
+                    self.printData(output.data(), 1, callback); \r
+                } else {\r
+                    var error = resp.error_output();\r
+                    if(error) {\r
+                        throw new Error("Error creating receipt: " + error.data());\r
+                    } else {\r
+                        throw new Error("No receipt data returned from server");\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    );\r
+}\r
+\r
+/**\r
+ * Print a receipt for this user's items out\r
+ */\r
+SelfCheckManager.prototype.printFinesReceipt = function(callback) {\r
+    progressDialog.show(true);\r
+\r
+    var params = [\r
+        this.authtoken, \r
+        this.staff.ws_ou(),\r
+        null,\r
+        'format.selfcheck.fines',\r
+        'print-on-demand',\r
+        [this.patron.id()]\r
+    ];\r
+\r
+    var self = this;\r
+    fieldmapper.standardRequest(\r
+        ['open-ils.circ', 'open-ils.circ.fire_user_trigger_events'],\r
+        {   \r
+            async : true,\r
+            params : params,\r
+            oncomplete : function(r) {\r
+                progressDialog.hide();\r
+                var resp = openils.Util.readResponse(r);\r
+                var output = resp.template_output();\r
+                if(output) {\r
+                    self.printData(output.data(), self.finesCount, callback); \r
+                } else {\r
+                    var error = resp.error_output();\r
+                    if(error) {\r
+                        throw new Error("Error creating receipt: " + error.data());\r
+                    } else {\r
+                        throw new Error("No receipt data returned from server");\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    );\r
+}\r
+\r
+\r
+/**\r
+ * Logout the patron and return to the login page\r
+ */\r
+SelfCheckManager.prototype.logoutPatron = function(print) {\r
+    progressDialog.show(true); // prevent patron from clicking logout link twice\r
+    if(print && this.checkouts.length) {\r
+        this.printSessionReceipt(\r
+            function() {\r
+                location.href = location.href;\r
+            }\r
+        );\r
+    } else {\r
+        location.href = location.href;\r
+    }\r
+}\r
+\r
+\r
+function checkLogin() {\r
+       selfCheckMgr.keepMeLoggedIn();\r
+       if(selfCheckMgr.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) {\r
+               switchTo('step2');\r
+               try{dojo.byId('patron-login-password').focus();}catch(e){}\r
+       } else {\r
+               selfCheckMgr.loginPatron(dojo.byId('patron-login-username').value);\r
+       }\r
+}\r
+\r
+\r
+function cancelLogin() {\r
+       dojo.byId('oils-selfck-status-div').innerHTML = '';\r
+       dojo.byId('oils-selfck-status-div2').innerHTML = '';\r
+       dojo.byId('oils-selfck-status-div3').innerHTML = '';\r
+       dojo.byId('patron-login-password').value = '';\r
+       openils.Util.hide('back_to_login');\r
+       switchTo('step1');\r
+       try {\r
+               dojo.byId('patron-login-username').focus();\r
+               dojo.byId('patron-login-username').select();\r
+       } catch(e) {}\r
+}\r
+\r
+/**\r
+ * Fire up the manager on page load\r
+ */\r
+openils.Util.addOnLoad(\r
+    function() {\r
+        new SelfCheckManager().init();\r
+               openils.Util.registerEnterHandler(dojo.byId('patron-login-username'), function(){checkLogin();});\r
+               openils.Util.registerEnterHandler(dojo.byId('patron-login-password'), function(){selfCheckMgr.loginPatron(dojo.byId('patron-login-username').value,dojo.byId('patron-login-password').value);});\r
+    }\r
+);\r