Self-check staff mode deployment prep
authorBill Erickson <berick@esilibrary.com>
Wed, 6 Feb 2013 14:30:53 +0000 (09:30 -0500)
committerBill Erickson <berick@esilibrary.com>
Wed, 6 Feb 2013 14:30:53 +0000 (09:30 -0500)
Indiscriminately copy templates and JS into temporaroy selfcheck.staff
directory for testing on a server without clobbering the existing
selfcheck UI.

To copy into place:

cp -r Open-ILS/src/templates/circ/selfcheck.staff /openils/var/templates/circ/
cp -r Open-ILS/web/css/skin/default/selfcheck.staff.css /openils/var/web/css/skin/default/
cp -r Open-ILS/web/js/ui/default/circ/selfcheck.staff /openils/var/web/js/ui/default/circ/
cp -r Open-ILS/web/js/dojo/openils/circ/nls/selfcheck.js /openils/var/web/js/dojo/openils/circ/nls/
cp Open-ILS/web/js/dojo/openils/Event.js /openils/var/web/js/dojo/openils/

Then add the permission..

Signed-off-by: Bill Erickson <berick@esilibrary.com>
13 files changed:
Open-ILS/src/templates/circ/selfcheck.staff/audio_config.tt2 [new file with mode: 0644]
Open-ILS/src/templates/circ/selfcheck.staff/banner.tt2 [new file with mode: 0644]
Open-ILS/src/templates/circ/selfcheck.staff/checkin_page.tt2 [new file with mode: 0644]
Open-ILS/src/templates/circ/selfcheck.staff/circ_page.tt2 [new file with mode: 0644]
Open-ILS/src/templates/circ/selfcheck.staff/fines.tt2 [new file with mode: 0644]
Open-ILS/src/templates/circ/selfcheck.staff/holds_page.tt2 [new file with mode: 0644]
Open-ILS/src/templates/circ/selfcheck.staff/main.tt2 [new file with mode: 0644]
Open-ILS/src/templates/circ/selfcheck.staff/patron_login.tt2 [new file with mode: 0644]
Open-ILS/src/templates/circ/selfcheck.staff/payment.tt2 [new file with mode: 0644]
Open-ILS/src/templates/circ/selfcheck.staff/summary.tt2 [new file with mode: 0644]
Open-ILS/web/css/skin/default/selfcheck.staff.css [new file with mode: 0644]
Open-ILS/web/js/ui/default/circ/selfcheck.staff/payment.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/circ/selfcheck.staff/selfcheck.js [new file with mode: 0644]

diff --git a/Open-ILS/src/templates/circ/selfcheck.staff/audio_config.tt2 b/Open-ILS/src/templates/circ/selfcheck.staff/audio_config.tt2
new file mode 100644 (file)
index 0000000..fc1bdef
--- /dev/null
@@ -0,0 +1,13 @@
+[%#
+    Override the audio config values by copying this template into your local templates
+    directory (matching the relative path) and change the values accordingly.
+#%]
+
+<script type="text/javascript">
+    SelfCheckManager.audioConfig = {
+        'login-success' : '',
+        'login-failure' : '[% ctx.media_prefix %]/audio/question.wav',
+        'checkout-success' : '[% ctx.media_prefix %]/audio/bonus.wav',
+        'checkout-failure' : '[% ctx.media_prefix %]/audio/question.wav',
+    }
+</script>
diff --git a/Open-ILS/src/templates/circ/selfcheck.staff/banner.tt2 b/Open-ILS/src/templates/circ/selfcheck.staff/banner.tt2
new file mode 100644 (file)
index 0000000..77a7104
--- /dev/null
@@ -0,0 +1,18 @@
+<div id='oils-selfck-user-info'>
+    <div id='oils-selfck-user-banner'></div>
+    <div id='oils-selfck-user-details' class='hidden'>
+        <div id='oils-selfck-user-name'></div>
+        <div id='oils-selfck-user-home-ou'></div>
+        <div id='oils-selfck-user-profile'></div>
+        <div id='oils-selfck-user-address'></div>
+    </div>
+</div>
+<div id='oils-selfck-logo-div'>
+    <img src='[% ctx.media_prefix %]/images/eg_logo.jpg'/>
+</div>
+<div id='oils-selfck-scan-div'>
+    <div id='oils-selfck-scan-text'></div>
+    <input jsId='selfckScanBox' dojoType='dijit.form.TextBox'/>
+</div>
+<div id='oils-selfck-status-div'></div>
+
diff --git a/Open-ILS/src/templates/circ/selfcheck.staff/checkin_page.tt2 b/Open-ILS/src/templates/circ/selfcheck.staff/checkin_page.tt2
new file mode 100644 (file)
index 0000000..5ccb268
--- /dev/null
@@ -0,0 +1,47 @@
+<div id='oils-selfck-checkin-table-div'>
+    <div id='oils-selfck-checkin-mods'>
+        <span class='oils-selfck-checkin-mod'>
+            <span>[% l('Backdate') %]</span>
+            <span><input 
+                id='oils-selfchk-backdate'
+                style='width: 8em;'
+                constraints='{max: new Date()}'
+                dojoType='dijit.form.DateTextBox' 
+                jsId='checkinBackdateInput'/>
+            </span>
+        </span>
+        <span class='oils-selfck-checkin-mod'>
+            <span>[% l('Amnesty Mode') %]</span>
+            <span><input 
+                id='oils-selfchk-amnesty-mode' 
+                type='checkbox' 
+                checked='checked'/>
+            </span>
+        </span>
+    </div>
+    <table id='oils-selfck-checkin-table' class='oils-selfck-item-table'>
+        <thead>
+            <tr>
+                <td id='oils-self-checkin-pic-cell'></td>
+                <td>[% l('Barcode') %]</td>
+                <td>[% l('Title') %]</td>
+                <td>[% l('Author') %]</td>
+                <td>[% l('Due Date') %]</td>
+                <td>[% l('Copy Status') %]</td>
+                <td>[% l('Outcome') %]</td>
+            </tr>
+        </thead>
+        <tbody id='oils-selfck-checkin-tbody'>
+            <tr id='oils-selfck-checkin-row'>
+                <td><img class='oils-selfck-jacket' name='jacket'/></td>
+                <td name='barcode'></td>
+                <td name='title'><img src='/opac/images/progressbar_green.gif'/></td>
+                <td name='author'></td>
+                <td name='due_date'></td>
+                <td name='status'></td>
+                <td name='outcome'></td>
+            </tr>
+        </tbody>
+        <tbody id='oils-selfck-checkin-out-tbody' class='oils-selfck-item-table'></tbody>
+    </table>
+</div>
diff --git a/Open-ILS/src/templates/circ/selfcheck.staff/circ_page.tt2 b/Open-ILS/src/templates/circ/selfcheck.staff/circ_page.tt2
new file mode 100644 (file)
index 0000000..3254e4e
--- /dev/null
@@ -0,0 +1,30 @@
+<div id='oils-selfck-circ-table-div'>
+    <table id='oils-selfck-circ-table' class='oils-selfck-item-table'>
+        <thead>
+            <tr>
+                <td id='oils-self-circ-pic-cell'></td>
+                <td>[% l('Barcode') %]</td>
+                <td>[% l('Title') %]</td>
+                <td>[% l('Author') %]</td>
+                <td>[% l('Due Date') %]</td>
+                <td class='hidden'>[% l('Renewals Left') %]</td>
+                <td>[% l('Type') %]</td>
+            </tr>
+        </thead>
+        <tbody id='oils-selfck-circ-tbody'>
+            <tr id='oils-selfck-circ-row'>
+                <td><img class='oils-selfck-jacket' name='jacket'/></td>
+                <td name='barcode'></td>
+                <td name='title'></td>
+                <td name='author'></td>
+                <td name='due_date'></td>
+                <td class='hidden' name='remaining'></td>
+                <td>
+                    <div name='checkout' class='hidden'>[% l('Checkout') %]</div>
+                    <div name='renew' class='hidden'>[% l('Renewal') %]</div>
+                </td>
+            </tr>
+        </tbody>
+        <tbody id='oils-selfck-circ-out-tbody' class='oils-selfck-item-table'></tbody>
+    </table>
+</div>
diff --git a/Open-ILS/src/templates/circ/selfcheck.staff/fines.tt2 b/Open-ILS/src/templates/circ/selfcheck.staff/fines.tt2
new file mode 100644 (file)
index 0000000..25b54f1
--- /dev/null
@@ -0,0 +1,26 @@
+<span id='oils-selfck-selected-total'></span>
+<span style='padding-left:5px;'><a class='hidden' href='javascript:void(0);' id='oils-selfck-pay-fines-link'>[% l('Pay Fines') %]</a></span>
+<div id='oils-selfck-fines-table-div'>
+    <table id='oils-selfck-fines-table' class='oils-selfck-item-table'>
+        <thead>
+            <tr>
+                <td><input type='checkbox' checked='checked' id='oils-selfck-fines-selector'/></td>
+                <td>[% l('Type') %]</td>
+                <td>[% l('Details') %]</td>
+                <td>[% l('Total Billed') %]</td>
+                <td>[% l('Total Paid') %]</td>
+                <td>[% l('Balance Owed') %]</td>
+            </tr>
+        </thead>
+        <tbody id='oils-selfck-fines-tbody'>
+            <tr id='oils-selfck-fines-row'>
+                <td><input type='checkbox' name='selector' checked='checked'/></td>
+                <td name='type'></td>
+                <td name='details'></td>
+                <td name='total_owed'></td>
+                <td name='total_paid'></td>
+                <td name='balance' style='color:red;'></td>
+            </tr>
+        </tbody>
+    </table>
+</div>
diff --git a/Open-ILS/src/templates/circ/selfcheck.staff/holds_page.tt2 b/Open-ILS/src/templates/circ/selfcheck.staff/holds_page.tt2
new file mode 100644 (file)
index 0000000..601c45d
--- /dev/null
@@ -0,0 +1,20 @@
+<div id='oils-selfck-hold-table-div'>
+    <table id='oils-selfck-hold-table' class='oils-selfck-item-table'>
+        <thead>
+            <tr>
+                <td id='oils-self-hold-pic-cell'></td>
+                <td>[% l('Title') %]</td>
+                <td>[% l('Author') %]</td>
+                <td>[% l('Status') %]</td>
+            </tr>
+        </thead>
+        <tbody id='oils-selfck-hold-tbody'>
+            <tr id='oils-selfck-hold-row'>
+                <td><img class='oils-selfck-jacket' name='jacket'/></td>
+                <td name='title'></td>
+                <td name='author'></td>
+                <td name='status'></td>
+            </tr>
+        </tbody>
+    </table>
+</div>
diff --git a/Open-ILS/src/templates/circ/selfcheck.staff/main.tt2 b/Open-ILS/src/templates/circ/selfcheck.staff/main.tt2
new file mode 100644 (file)
index 0000000..521fba7
--- /dev/null
@@ -0,0 +1,71 @@
+[% ctx.page_title = l('Self Checkout') %]
+[% WRAPPER base.tt2 %]
+<script type="text/javascript" src='[% ctx.media_prefix %]/js/ui/default/circ/selfcheck.staff/selfcheck.js'> </script>
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/circ/selfcheck/payment.js"></script>
+<link rel='stylesheet' type='text/css' href='[% ctx.media_prefix %]/css/skin/[% ctx.skin %]/selfcheck.staff.css'/>
+[% INCLUDE 'circ/selfcheck.staff/audio_config.tt2' %]
+
+<div id='oils-selfck-top-div'>
+    [% INCLUDE 'circ/selfcheck.staff/banner.tt2' %]
+</div>
+<div id='oils-selfck-bottom-div'>
+    <div id='oils-selfck-content-div'>
+        <div id='oils-selfck-content-header'>
+            <span><a id='oils-selfck-print-list-link' href='javascript:void(0);'>[% l('Print List') %]</a></span>
+        </div>
+        <div id='oils-selfck-circ-page' class='hidden'>
+            <!-- Checkout / renewal and items out interface -->
+            [% INCLUDE 'circ/selfcheck.staff/circ_page.tt2' %]
+        </div>
+        <div id='oils-selfck-checkin-page' class='hidden'>
+            <!-- Checkin interface; Staff only -->
+            [% INCLUDE 'circ/selfcheck.staff/checkin_page.tt2' %]
+        </div>
+        <div id='oils-selfck-holds-page' class='hidden'>
+            <!-- Patron holds interface -->
+            [% INCLUDE 'circ/selfcheck.staff/holds_page.tt2' %]
+        </div>
+        <div id='oils-selfck-fines-page' class='hidden'>
+            <!-- Fines and interface -->
+            [% INCLUDE 'circ/selfcheck.staff/fines.tt2' %]
+        </div>
+        <div id='oils-selfck-payment-page' class='hidden'>
+            <!-- credit card payments interface -->
+            [% INCLUDE 'circ/selfcheck.staff/payment.tt2' %]
+        </div>
+    </div>
+    <div id='oils-selfck-summary-div'>
+        [% INCLUDE 'circ/selfcheck.staff/summary.tt2' %]
+    </div>
+</div>
+<div id='oils-selfchk-staff-actions' class='hidden'>
+    <span>
+        <a id='oils-selfchk-staff-logout' 
+            href='javascript:;'>[% l('Staff Logout') %]</a>
+    </span>
+</div>
+<div dojoType='openils.widget.ProgressDialog' jsId='progressDialog'></div>
+<div dojoType="dijit.Dialog" jsId='oilsSelfckWsDialog' class='oils-login-dialog' style='display:none;'>
+    <form>
+        <table>
+            <tr>
+                <td>[% l('Choose a location') %]</td>
+                <td><div dojoType='openils.widget.OrgUnitFilteringSelect' jsId='oilsSelfckWsLocSelector' 
+                    searchAttr='shortname' labelAttr='shortname'></div></td>
+            </tr>
+            <tr>
+                <td>[% l('Enter a workstation name') %]</td>
+                <td><input dojoType='dijit.form.TextBox' jsId='oilsSelfckWsName'/></td>
+            </tr>
+            <tr>
+                <td colspan='2' align='center'>
+                    <button jsId='oilsSelfckWsSubmit' dojoType='dijit.form.Button'>[% l('Submit') %]</button>
+                </td>
+            </tr>
+        </table>
+    </form>
+</div>
+[% END %]
+
+
+
diff --git a/Open-ILS/src/templates/circ/selfcheck.staff/patron_login.tt2 b/Open-ILS/src/templates/circ/selfcheck.staff/patron_login.tt2
new file mode 100644 (file)
index 0000000..7009f9d
--- /dev/null
@@ -0,0 +1,7 @@
+<div>[% l('Please login using your library barcode') %]</div>
+<div class='oils-selfck-login-box'>
+    <input jsId='selfckBarcodeBox' dojoType='dijit.form.TextBox'></input>
+</div>
+<div id='oils-selfck-login-pw' class='hidden oils-selfck-login-box'>
+    <input jsId='selfckPwBox' dojoType='dijit.form.TextBox'></input>
+</div>
diff --git a/Open-ILS/src/templates/circ/selfcheck.staff/payment.tt2 b/Open-ILS/src/templates/circ/selfcheck.staff/payment.tt2
new file mode 100644 (file)
index 0000000..c04342a
--- /dev/null
@@ -0,0 +1,83 @@
+<div id='oils-selfck-cc-payment-summary'>
+    [% l('Total amount to pay: $[_1]', '<span></span>') %]
+</div>
+<table id='oils-selfck-cc-payment-table'>
+    <tbody>
+        <!-- Technically not needed since card type is derived from the CC number
+        <tr>
+            <td>[% l('Type of Card') %]</td>
+            <td>
+                <select dojoType='dijit.form.FilteringSelect' jsId='oilsSelfckCCType' required='true'>
+                    <option value='VISA'>[% l('VISA') %]</option>
+                    <option value='MasterCard'>[% l('MasterCard') %]</option>
+                    <option value='American Express'>[% l('American Express') %]</option>
+                </select>
+            </td>
+        </tr>
+        -->
+        <tr>
+            <td>[% l('Credit Card #') %]</td>
+            <td><input dojoType='dijit.form.TextBox' jsId='oilsSelfckCCNumber' required='true'/></td>
+        </tr>
+        <tr>
+            <td>[% l('CVV #') %]</td>
+            <td><input dojoType='dijit.form.TextBox' jsId='oilsSelfckCCCVV' required='true'/></td>
+        </tr>
+        <tr>
+            <td>[% l('Expiration Month') %]</td>
+            <td>
+                <select dojoType='dijit.form.FilteringSelect' jsId='oilsSelfckCCMonth' required='true'>
+                    <option value='01' selected='selected'>[% l('Jan') %]</option>
+                    <option value='02'>[% l('Feb') %]</option>
+                    <option value='03'>[% l('Mar') %]</option>
+                    <option value='04'>[% l('April') %]</option>
+                    <option value='05'>[% l('May') %]</option>
+                    <option value='06'>[% l('June') %]</option>
+                    <option value='07'>[% l('July') %]</option>
+                    <option value='08'>[% l('Aug') %]</option>
+                    <option value='09'>[% l('Sept') %]</option>
+                    <option value='10'>[% l('Oct') %]</option>
+                    <option value='11'>[% l('Nov') %]</option>
+                    <option value='12'>[% l('Dec') %]</option>
+                </select>
+            </td>
+        </tr>
+        <tr>
+            <td>[% l('Expiration Year') %]</td>
+            <td><input dojoType='dijit.form.NumberSpinner' constraints='{pattern:"0000", places:0, maxlength:4}' jsId='oilsSelfckCCYear' required='true'/></td>
+        </tr>
+        <tr>
+            <td>[% l('Edit Billing Details') %]</td>
+            <td><input dojoType='dijit.form.CheckBox' jsId='oilsSelfckEditDetails'/></td>
+        </tr>
+        <tr>
+            <td>[% l('First Name') %]</td>
+            <td><input dojoType='dijit.form.TextBox' jsId='oilsSelfckCCFName' disabled='disabled' required='true'/></td>
+        </tr>
+        <tr>
+            <td>[% l('Last Name') %]</td>
+            <td><input dojoType='dijit.form.TextBox' jsId='oilsSelfckCCLName' disabled='disabled' required='true'/></td>
+        </tr>
+        <tr>
+            <td>[% l('Street Address') %]</td>
+            <td><input dojoType='dijit.form.TextBox' jsId='oilsSelfckCCStreet' disabled='disabled' required='true'/></td>
+        </tr>
+        <tr>
+            <td>[% l('City') %]</td>
+            <td><input dojoType='dijit.form.TextBox' jsId='oilsSelfckCCCity' disabled='disabled' required='true'/></td>
+        </tr>
+        <tr>
+            <td>[% l('State or Province') %]</td>
+            <td><input dojoType='dijit.form.TextBox' jsId='oilsSelfckCCState' disabled='disabled' required='true'/></td>
+        </tr>
+        <tr>
+            <td>[% l('ZIP or Postal Code') %]</td>
+            <td><input dojoType='dijit.form.TextBox' jsId='oilsSelfckCCZip' disabled='disabled' required='true'/></td>
+        </tr>
+        <tr>
+            <td colspan='2' align='center'>
+                <button dojoType='dijit.form.Button' jsId='oilsSelfckCCSubmit'>[% l('Submit Payment') %]</button>
+            </td>
+        </tr>
+    </tbody>
+</table>
diff --git a/Open-ILS/src/templates/circ/selfcheck.staff/summary.tt2 b/Open-ILS/src/templates/circ/selfcheck.staff/summary.tt2
new file mode 100644 (file)
index 0000000..c67f23a
--- /dev/null
@@ -0,0 +1,28 @@
+<div id='oils-selfck-circ-info-div'>
+    <div id='oils-selfck-info-nav'>
+        <span><a id='oils-selfck-nav-home' href='javascript:void(0);' class='selected'>[% l('Home') %]</a></span>
+        <span class='hidden'><a id='oils-selfck-nav-checkin' href='javascript:void(0);'>[% l('Checkin') %]</a></span>
+        <span><a id='oils-selfck-nav-logout-print' href='javascript:void(0);'>[% l('Logout') %]</a></span>
+        <span><a id='oils-selfck-nav-logout' href='javascript:void(0);'>[% l('Logout (No Receipt)') %]</a></span>
+    </div>
+    <fieldset>
+        <legend>[% l('Items Checked Out') %]</legend>
+        <div id='oils-selfck-circ-session-total'></div>
+        <div id='oils-selfck-circ-account-total'></div>
+        <div><a href='javascript:void(0);' id='oils-selfck-items-out-details-link'>[% l('View Items Out') %]</a></div>
+    </fieldset>
+    <fieldset>
+        <legend>[% l('Holds') %]</legend>
+        <div id='oils-selfck-holds-ready'></div>
+        <div id='oils-selfck-holds-total'></div>
+        <div><a href='javascript:void(0);' id='oils-selfck-hold-details-link'>[% l('View Holds') %]</a></div>
+    </fieldset>
+    <fieldset>
+        <legend>[% l('Fines') %]</legend>
+        <div id='oils-selfck-fines-total'></div>
+        <div>
+            <span><a href='javascript:void(0);' id='oils-selfck-view-fines-link'>[% l('View Details') %]</a></span>
+        </div>
+    </fieldset>
+</div>
+
diff --git a/Open-ILS/web/css/skin/default/selfcheck.staff.css b/Open-ILS/web/css/skin/default/selfcheck.staff.css
new file mode 100644 (file)
index 0000000..063a846
--- /dev/null
@@ -0,0 +1,174 @@
+
+body {
+    font-family: Arial, Verdana;
+    font-size: 13px;
+}
+
+#oils-selfck-top-div {
+    padding: 20px;
+    margin: 2px;
+    border: 1px solid #888;
+    text-align: center;
+    font-weight:bold;
+}
+
+#oils-selfck-user-info {
+    position:fixed;
+    top:30px;
+    right:30px;
+}
+
+#oils-selfck-user-details {
+    text-align: left;
+    padding-right: 15px;
+}
+
+#oils-selfck-logo-div {
+    margin: 20px;
+}
+
+#oils-selfck-scan-text {
+    margin: 10px;
+}
+
+#oils-selfck-bottom-div {
+    width: 98%;
+    padding: 10px;
+}
+
+#oils-self-circ-pic-cell {
+    width: 43px;
+}
+
+.oils-selfck-jacket {
+    height: 50px; 
+    width: 40px;  
+    border: none;
+}
+
+.oils-selfck-item-table  {
+    width: 98%;
+    margin-top: 15px;
+}
+
+.oils-selfck-item-table td {
+    text-align: left;
+    padding: 7px;
+}
+
+.oils-selfck-item-table thead {
+    font-weight: bold;
+}
+
+.oils-selfck-item-table tbody tr {
+    border-bottom: 1px solid #888;
+}
+
+
+#oils-selfck-content-div {
+    width: 70%;
+    position: float;
+    float: left;
+}
+
+#oils-selfck-summary-div {
+    width: 28%;
+    float: right;
+    border-left: 1px solid #888;
+}
+
+#oils-selfck-content-header {
+    margin: 8px 10px 15px 10px;
+    padding: 10px 0 3px 0;
+    border-bottom: 1px dashed #888;
+    text-align: right;
+    line-height: 200%;  /* to line up with #oils-selfck-info-nav span */
+    width: 100%;
+}
+#oils-selfck-content-header span {
+    padding-left: 5px;
+}
+
+#oils-selfck-info-nav {
+    margin: 15px 10px 15px 10px;
+    padding: 8px;
+    border-bottom: 1px dashed #888;
+}
+
+#oils-selfck-circ-info-div span {
+    padding-right: 8px;
+}
+
+
+#oils-selfck-info-nav span {
+    /* padding-left: 8px; */
+    line-height: 200%;
+}
+
+#oils-selfck-info-nav a {
+    padding-left: 4px;
+    padding-right: 4px;
+    padding-top: 4px;
+    white-space: nowrap;
+}
+
+#oils-selfck-info-nav .selected {
+    background-color: #e0e0e0;
+    border: 1px solid #333;
+}
+
+#oils-selfck-circ-info-div fieldset {
+    margin: 15px 10px 15px 10px;
+    padding: 8px;
+    border: 2px dashed #888;
+    -moz-border-radius: 3px;
+}
+
+#oils-selfck-circ-info-div fieldset legend {
+    font-weight: bold;
+}
+
+#oils-selfck-circ-info-div div {
+    padding: 3px;
+}
+
+#oils-selfck-status-div {
+    height: 20px;
+    padding: 10px;
+    font-weight: bold;
+    vertical-align: middle;
+    color: red;
+}
+
+#oils-selfck-cc-payment-summary {
+    font-weight:bold;
+    padding: 30px;
+}
+
+#oils-selfck-cc-payment-table td {
+    padding: 5px;
+}
+
+#oils-selfck-checkin-mods {
+   text-align: right;
+   border-bottom: 1px solid #333;
+   background-color: #e0e0e0;
+   padding: 10px;
+}
+
+.oils-selfck-checkin-mod {
+    border-left: 1px solid #333;
+    padding-left: 8px;
+    padding-left: 8px;
+}
+
+#oils-selfchk-staff-actions {
+    position: absolute;
+    bottom: 0px;
+    right: 0px;
+    text-align: right;
+    padding-left: 10px;
+    padding: 10px;
+    background-color: #e0e0e0;
+    border-top: 2px solid #333;
+}
diff --git a/Open-ILS/web/js/ui/default/circ/selfcheck.staff/payment.js b/Open-ILS/web/js/ui/default/circ/selfcheck.staff/payment.js
new file mode 100644 (file)
index 0000000..1871bba
--- /dev/null
@@ -0,0 +1,117 @@
+function PaymentForm() {}
+var proto = (typeof(SelfCheckManager) == "undefined" ?
+    PaymentForm : SelfCheckManager).prototype;
+
+proto.drawPayFinesPage = function(patron, total, xacts, onPaymentSubmit) {
+    if (typeof(this.authtoken) == "undefined")
+        this.authtoken = patron.session;
+
+    dojo.query("span", "oils-selfck-cc-payment-summary")[0].innerHTML = total;
+
+    oilsSelfckCCNumber.attr('value', '');
+    oilsSelfckCCCVV.attr('value', '');
+    oilsSelfckCCMonth.attr('value', '01');
+    oilsSelfckCCYear.attr('value', new Date().getFullYear());
+    oilsSelfckCCFName.attr('value', patron.first_given_name());
+    oilsSelfckCCLName.attr('value', patron.family_name());
+
+    var addr = patron.billing_address() || patron.mailing_address();
+
+    if (typeof(addr) != "object") {
+        /* still don't have usable address? try getting better user object. */
+        fieldmapper.standardRequest(
+            ["open-ils.actor", "open-ils.actor.user.fleshed.retrieve"], {
+                "params": [
+                    patron.session, patron.id(), [
+                        "billing_address", "mailing_address"
+                    ]
+                ],
+                "async": false,
+                "oncomplete": function(r) {
+                    var usr = openils.Util.readResponse(r);
+                    if (usr)
+                        addr = usr.billing_address() || usr.mailing_address();
+                }
+            }
+        );
+    }
+
+    if (addr) {
+        oilsSelfckCCStreet.attr('value', addr.street1()+' '+addr.street2());
+        oilsSelfckCCCity.attr('value', addr.city());
+        oilsSelfckCCState.attr('value', addr.state());
+        oilsSelfckCCZip.attr('value', addr.post_code());
+    }
+
+    dojo.connect(oilsSelfckEditDetails, 'onChange',
+        function(newVal) {
+            dojo.forEach(
+                [   oilsSelfckCCFName,
+                    oilsSelfckCCLName,
+                    oilsSelfckCCStreet,
+                    oilsSelfckCCCity,
+                    oilsSelfckCCState,
+                    oilsSelfckCCZip
+                ],
+                function(dij) { dij.attr('disabled', !newVal); }
+            );
+        }
+    );
+
+
+    var self = this;
+    dojo.connect(oilsSelfckCCSubmit, 'onClick',
+        function() {
+            /* XXX better to replace this check on progressDialog with some
+             * kind of passed-in function to support different use cases */
+            if (typeof(progressDialog) != "undefined")
+                progressDialog.show(true);
+
+            self.sendCCPayment(patron, xacts, onPaymentSubmit);
+        }
+    );
+}
+
+// In this form, this code only supports global on/off credit card
+// payments and does not dissallow payments to transactions that started
+// at remote locations or transactions that have accumulated billings at
+// remote locations that dissalow credit card payments.
+// TODO add per-transaction blocks for orgs that do not support CC payments
+
+proto.sendCCPayment = function(patron, xacts, onPaymentSubmit) {
+
+    var args = {
+        userid : patron.id(),
+        payment_type : 'credit_card_payment',
+        payments : xacts,
+        cc_args : {
+            where_process : 1,
+            //type : oilsSelfckCCType.attr('value'),
+            number : oilsSelfckCCNumber.attr('value'),
+            cvv2 : oilsSelfckCCCVV.attr('value'),
+            expire_year : oilsSelfckCCYear.attr('value'),
+            expire_month : oilsSelfckCCMonth.attr('value'),
+            billing_first : oilsSelfckCCFName.attr('value'),
+            billing_last : oilsSelfckCCLName.attr('value'),
+            billing_address : oilsSelfckCCStreet.attr('value'),
+            billing_city : oilsSelfckCCCity.attr('value'),
+            billing_state : oilsSelfckCCState.attr('value'),
+            billing_zip : oilsSelfckCCZip.attr('value')
+        }
+    }
+
+    var resp = fieldmapper.standardRequest(
+        ['open-ils.circ', 'open-ils.circ.money.payment'],
+        {params : [this.authtoken, args, patron.last_xact_id()]}
+    );
+
+    if (typeof(progressDialog) != "undefined")
+        progressDialog.hide();
+
+    if (typeof(onPaymentSubmit) == "function") {
+        onPaymentSubmit(resp);
+    } else {
+        var evt = openils.Event.parse(resp);
+        if (evt) alert(evt);
+    }
+}
diff --git a/Open-ILS/web/js/ui/default/circ/selfcheck.staff/selfcheck.js b/Open-ILS/web/js/ui/default/circ/selfcheck.staff/selfcheck.js
new file mode 100644 (file)
index 0000000..5d6f7a0
--- /dev/null
@@ -0,0 +1,1795 @@
+dojo.require('dojo.date.locale');
+dojo.require('dojo.cookie');
+dojo.require('dojo.date.stamp');
+dojo.require('dijit.form.CheckBox');
+dojo.require('dijit.form.DateTextBox');
+dojo.require('dijit.form.NumberSpinner');
+dojo.require('openils.CGI');
+dojo.require('openils.Util');
+dojo.require('openils.User');
+dojo.require('openils.Event');
+dojo.require('openils.PermaCrud');
+dojo.require('openils.widget.ProgressDialog');
+dojo.require('openils.widget.OrgUnitFilteringSelect');
+
+
+dojo.requireLocalization('openils.circ', 'selfcheck');
+var localeStrings = dojo.i18n.getLocalization('openils.circ', 'selfcheck');
+
+
+const SET_BARCODE_REGEX = 'opac.barcode_regex';
+const SET_PATRON_TIMEOUT = 'circ.selfcheck.patron_login_timeout';
+const SET_AUTO_OVERRIDE_EVENTS = 'circ.selfcheck.auto_override_checkout_events';
+const SET_PATRON_PASSWORD_REQUIRED = 'circ.selfcheck.patron_password_required';
+const SET_AUTO_RENEW_INTERVAL = 'circ.checkout_auto_renew_age';
+const SET_WORKSTATION_REQUIRED = 'circ.selfcheck.workstation_required';
+const SET_ALERT_POPUP = 'circ.selfcheck.alert.popup';
+const SET_ALERT_SOUND = 'circ.selfcheck.alert.sound';
+const SET_CC_PAYMENT_ALLOWED = 'credit.payments.allow';
+// This setting only comes into play if COPY_NOT_AVAILABLE is in the SET_AUTO_OVERRIDE_EVENTS list
+const SET_BLOCK_CHECKOUT_ON_COPY_STATUS = 'circ.selfcheck.block_checkout_on_copy_status';
+
+// set before the login dialog is rendered
+openils.User.default_login_agent = 'selfcheck';
+
+function SelfCheckManager() {
+
+    this.cgi = new openils.CGI();
+    this.staff = null; 
+    this.workstation = null;
+    this.authtoken = null;
+
+    this.patron = null; 
+    this.patronBarcodeRegex = null;
+
+    this.checkouts = [];
+    this.itemsOut = [];
+
+    // During renewals, keep track of the ID of the previous circulation. 
+    // Previous circ is used for tracking failed renewals (for receipts).
+    this.prevCirc = null;
+
+    // current item barcode
+    this.itemBarcode = null; 
+
+    // are we currently performing a renewal?
+    this.isRenewal = false; 
+
+    // dict of org unit settings for "here"
+    this.orgSettings = {};
+
+    // true if we are exposing staff-only features (e.g. checkin)
+    this.staffMode = false;
+
+    this.copyStatusMap = {};
+
+    // Construct a mock checkout for debugging purposes
+    if(this.mockCheckouts = this.cgi.param('mock-circ')) {
+
+        this.mockCheckout = {
+            payload : {
+                record : new fieldmapper.mvr(),
+                copy : new fieldmapper.acp(),
+                circ : new fieldmapper.circ()
+            }
+        };
+
+        this.mockCheckout.payload.record.title('Jazz improvisation for guitar');
+        this.mockCheckout.payload.record.author('Wise, Les');
+        this.mockCheckout.payload.record.isbn('0634033565');
+        this.mockCheckout.payload.copy.barcode('123456789');
+        this.mockCheckout.payload.circ.renewal_remaining(1);
+        this.mockCheckout.payload.circ.parent_circ(1);
+        this.mockCheckout.payload.circ.due_date('2012-12-21');
+    }
+
+    this.initPrinter();
+}
+
+SelfCheckManager.prototype.setupStaffLogin = function(verify) {
+
+    if(verify) oilsSetupUser(); 
+    this.staff = openils.User.user;
+    this.workstation = openils.User.workstation;
+    this.authtoken = openils.User.authtoken;
+}
+
+SelfCheckManager.prototype.logoutStaff = function() {
+    // remove the authtoken
+    dojo.cookie('ses', null, {expires:-1, path:'/'}); 
+    // reload the page, which displays the login dialog
+    location.href = location.href;
+}
+
+SelfCheckManager.prototype.activateStaffMode = function(permorgs) {
+    var self = this;
+
+    // make sure this staff account has the needed permission here
+    if (permorgs.indexOf(Number(this.staff.ws_ou())) == -1) return;
+
+    this.staffMode = true;
+
+    openils.Util.show(
+        dojo.byId('oils-selfck-nav-checkin').parentNode, 
+        'inline'
+    );
+
+    openils.Util.show(dojo.byId('oils-selfchk-staff-actions'));
+
+    dojo.byId('oils-selfchk-staff-logout').onclick = function() {
+        self.logoutStaff();
+    };
+}
+
+
+/**
+ * Fetch the org-unit settings, initialize the display, etc.
+ */
+SelfCheckManager.prototype.init = function() {
+    var self = this;
+
+    this.setupStaffLogin();
+    this.loadOrgSettings();
+    this.loadCopyStatuses();
+
+    // are we in staff mode?
+    new openils.User().getPermOrgList(['SELFCHECK_STAFF_MODE'], 
+        function(orglist) { self.activateStaffMode(orglist) },
+        true, true
+    );
+
+    this.circTbody = dojo.byId('oils-selfck-circ-tbody');
+    this.checkinTbody = dojo.byId('oils-selfck-checkin-tbody');
+    this.itemsOutTbody = dojo.byId('oils-selfck-circ-out-tbody');
+    this.itemsCheckinTbody = dojo.byId('oils-selfck-checkin-out-tbody');
+
+    // workstation is required but none provided
+    if(this.orgSettings[SET_WORKSTATION_REQUIRED] && !this.workstation) {
+        if(confirm(dojo.string.substitute(localeStrings.WORKSTATION_REQUIRED))) {
+            this.registerWorkstation();
+        }
+        return;
+    }
+    
+    // connect onclick handlers to the various navigation links
+    var linkHandlers = {
+        'oils-selfck-hold-details-link' : function() { self.drawHoldsPage(); },
+        'oils-selfck-view-fines-link' : function() { self.drawFinesPage(); },
+        'oils-selfck-pay-fines-link' : function() {
+            self.goToTab("payment");
+            self.drawPayFinesPage(
+                self.patron,
+                self.getSelectedFinesTotal(),
+                self.getSelectedFineTransactions(),
+                function(resp) {
+                    var evt = openils.Event.parse(resp);
+                    if(evt) {
+                        var message = evt + '';
+                        if(evt.textcode == 'CREDIT_PROCESSOR_DECLINED_TRANSACTION' && evt.payload)
+                            message += '\n' + evt.payload.error_message;
+                        if(evt.textcode == 'INVALID_USER_XACT_ID')
+                            message += '\n' + localeStrings.PAYMENT_INVALID_USER_XACT_ID;
+                        self.handleAlert(message, true, 'payment-failure');
+                        return;
+                    }
+
+                    self.patron.last_xact_id(resp.last_xact_id); // update to match latest from server
+                    self.printPaymentReceipt(
+                        resp,
+                        function() {
+                            self.updateFinesSummary();
+                            self.drawFinesPage();
+                        }
+                    );
+                }
+            );
+        },
+        'oils-selfck-nav-home' : function() { self.drawCircPage(); },
+        'oils-selfck-nav-checkin' : function() { self.drawCheckinPage(); },
+        'oils-selfck-nav-logout' : function() { self.logoutPatron(); },
+        'oils-selfck-nav-logout-print' : function() { self.logoutPatron(true); },
+        'oils-selfck-items-out-details-link' : function() { self.drawItemsOutPage(); },
+        'oils-selfck-print-list-link' : function() { self.printList(); }
+    }
+
+    for(var id in linkHandlers) 
+        dojo.connect(dojo.byId(id), 'onclick', linkHandlers[id]);
+
+
+    if(this.cgi.param('patron')) {
+        
+        // Patron barcode via cgi param.  Mainly used for debugging and
+        // only works if password is not required by policy
+        this.loginPatron(this.cgi.param('patron'));
+
+    } else {
+        this.drawLoginPage();
+    }
+
+    /**
+     * To test printing, pass a URL param of 'testprint'.  The value for the param
+     * should be a JSON string like so:  [{circ:<circ_id>}, ...]
+     */
+    var testPrint = this.cgi.param('testprint');
+    if(testPrint) {
+        this.checkouts = JSON2js(testPrint);
+        this.printSessionReceipt();
+        this.checkouts = [];
+    }
+}
+
+
+SelfCheckManager.prototype.loadCopyStatuses = function() {
+    var self = this;
+    var pcrud = new openils.PermaCrud();
+    pcrud.retrieveAll('ccs', {
+        async : true,
+        oncomplete : function(r) {
+            var list = openils.Util.readResponse(r);
+            dojo.forEach(list, function(stat) {
+                self.copyStatusMap[stat.id()] = stat;
+            });
+        }
+    });
+};
+
+SelfCheckManager.prototype.getSelectedFinesTotal = function() {
+    var total = 0;
+    dojo.forEach(
+        dojo.query("[name=selector]", this.finesTbody),
+        function(input) {
+            if(input.checked)
+                total += Number(input.getAttribute("balance_owed"));
+        }
+    );
+    return total.toFixed(2);
+};
+
+SelfCheckManager.prototype.getSelectedFineTransactions = function() {
+    return dojo.query("[name=selector]", this.finesTbody).
+        filter(function (o) { return o.checked }).
+        map(
+            function (o) {
+                return [
+                    o.getAttribute("xact"),
+                    Number(o.getAttribute("balance_owed")).toFixed(2)
+                ];
+            }
+        );
+};
+
+/**
+ * Registers a new workstion
+ */
+SelfCheckManager.prototype.registerWorkstation = function() {
+    
+    oilsSelfckWsDialog.show();
+
+    new openils.User().buildPermOrgSelector(
+        'REGISTER_WORKSTATION', 
+        oilsSelfckWsLocSelector, 
+        this.staff.home_ou()
+    );
+
+
+    var self = this;
+    dojo.connect(oilsSelfckWsSubmit, 'onClick', 
+
+        function() {
+            oilsSelfckWsDialog.hide();
+            var name = oilsSelfckWsLocSelector.attr('displayedValue') + '-' + oilsSelfckWsName.attr('value');
+
+            var res = fieldmapper.standardRequest(
+                ['open-ils.actor', 'open-ils.actor.workstation.register'],
+                { params : [
+                        self.authtoken, name, oilsSelfckWsLocSelector.attr('value')
+                    ]
+                }
+            );
+
+            if(evt = openils.Event.parse(res)) {
+                if(evt.textcode == 'WORKSTATION_NAME_EXISTS') {
+                    if(confirm(localeStrings.WORKSTATION_EXISTS)) {
+                        location.href = location.href.replace(/\?.*/, '') + '?ws=' + name;
+                    } else {
+                        self.registerWorkstation();
+                    }
+                    return;
+                } else {
+                    alert(evt);
+                }
+            } else {
+                location.href = location.href.replace(/\?.*/, '') + '?ws=' + name;
+            }
+        }
+    );
+}
+
+/**
+ * Loads the org unit settings
+ */
+SelfCheckManager.prototype.loadOrgSettings = function() {
+
+    var settings = fieldmapper.aou.fetchOrgSettingBatch(
+        this.staff.ws_ou(), [
+            SET_BARCODE_REGEX,
+            SET_PATRON_TIMEOUT,
+            SET_ALERT_POPUP,
+            SET_ALERT_SOUND,
+            SET_AUTO_OVERRIDE_EVENTS,
+            SET_BLOCK_CHECKOUT_ON_COPY_STATUS,
+            SET_PATRON_PASSWORD_REQUIRED,
+            SET_AUTO_RENEW_INTERVAL,
+            SET_WORKSTATION_REQUIRED,
+            SET_CC_PAYMENT_ALLOWED
+        ]
+    );
+
+    for(k in settings) {
+        if(settings[k])
+            this.orgSettings[k] = settings[k].value;
+    }
+
+    if(settings[SET_BARCODE_REGEX]) 
+        this.patronBarcodeRegex = new RegExp(settings[SET_BARCODE_REGEX].value);
+}
+
+SelfCheckManager.prototype.drawLoginPage = function() {
+    var self = this;
+
+    var bcHandler = function(barcode_or_usrname) {
+        // handle patron barcode/usrname entry
+
+        if(self.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) {
+            
+            // password is required.  wire up the scan box to read it
+            self.updateScanBox({
+                msg : 'Please enter your password', // TODO i18n 
+                handler : function(pw) { self.loginPatron(barcode_or_usrname, pw); },
+                password : true
+            });
+
+        } else {
+            // password is not required, go ahead and login
+            self.loginPatron(barcode_or_usrname);
+        }
+    };
+
+    this.updateScanBox({
+        msg : 'Please log in with your username or library barcode.', // TODO
+        handler : bcHandler
+    });
+}
+
+/**
+ * Login the patron.  
+ */
+SelfCheckManager.prototype.loginPatron = function(barcode_or_usrname, passwd) {
+
+    this.setupStaffLogin(true); // verify still valid
+
+    var barcode = null;
+    var usrname = null;
+    console.log('testing ' + barcode_or_usrname);
+    if (barcode_or_usrname.match(this.patronBarcodeRegex)) {
+        console.log('barcode');
+        barcode = barcode_or_usrname;
+    } else {
+        console.log('usrname');
+        usrname = barcode_or_usrname;
+    }
+
+    if(this.orgSettings[SET_PATRON_PASSWORD_REQUIRED]) {
+        
+        if(!passwd) {
+            // would only happen in dev/debug mode when using the patron= param
+            alert('password required by org setting.  remove patron= from URL'); 
+            return;
+        }
+
+        // patron password is required.  Verify it.
+
+        var self = this;
+        new openils.User().auth_verify(
+            {   username : usrname, barcode : barcode, 
+                type : 'opac', passwd : passwd, agent : 'selfcheck' },
+            function(OK) {
+                if (OK) {
+                    self.fetchPatron(barcode, usrname);
+
+                } else {
+                    // auth verify failed
+                    self.handleAlert(
+                        dojo.string.substitute(localeStrings.LOGIN_FAILED, [barcode_or_usrname]),
+                        false, 'login-failure'
+                    );
+                    self.drawLoginPage();
+                }
+            }
+        );
+
+    } else {
+        this.fetchPatron(barcode, usrname);
+    }
+};
+
+SelfCheckManager.prototype.fetchPatron = function(barcode, usrname) {
+
+    var patron_id = fieldmapper.standardRequest(
+        ['open-ils.actor', 'open-ils.actor.user.retrieve_id_by_barcode_or_username'],
+        {params : [this.authtoken, barcode, usrname]}
+    );
+
+    // retrieve the fleshed user by id
+    this.patron = fieldmapper.standardRequest(
+        ['open-ils.actor', 'open-ils.actor.user.fleshed.retrieve.authoritative'],
+        {params : [this.authtoken, patron_id,
+            [   'card',                                                                
+                'cards',
+                'addresses',                                                           
+                'billing_address',                                                     
+                'mailing_address',  
+                'profile'
+            ]
+        ]}
+    );
+
+    var evt = openils.Event.parse(this.patron);
+    
+    // verify validity of the card used to log in
+    var inactiveCard = false;
+    if(!evt) {
+        var card;
+        if (barcode) {
+            card = this.patron.cards().filter(
+                function(c) { return (c.barcode() == barcode); })[0];
+        } else {
+            card = this.patron.card();
+        }
+        inactiveCard = !openils.Util.isTrue(card.active());
+    }
+
+    if(evt || inactiveCard) {
+        this.handleAlert(
+            dojo.string.substitute(localeStrings.LOGIN_FAILED, [barcode || usrname]),
+            false, 'login-failure'
+        );
+        this.drawLoginPage();
+
+    } else {
+
+        this.handleAlert('', false, 'login-success');
+
+        if (this.staffMode) {
+            this.drawPatronInfo();
+
+        } else {
+            dojo.byId('oils-selfck-user-banner').innerHTML = 
+                dojo.string.substitute(localeStrings.WELCOME_BANNER, 
+                    [this.patron.first_given_name()]);
+        }
+
+
+        this.drawCircPage();
+    }
+}
+
+
+SelfCheckManager.prototype.drawPatronInfo = function() {
+
+    openils.Util.show('oils-selfck-user-details');
+
+    var patron = this.patron;
+
+    dojo.byId('oils-selfck-user-name').innerHTML = 
+        openils.User.formalName(patron);
+
+    dojo.byId('oils-selfck-user-home-ou').innerHTML = 
+        fieldmapper.aou.findOrgUnit(patron.home_ou()).shortname();
+
+    dojo.byId('oils-selfck-user-profile').innerHTML = 
+        patron.profile().name();
+
+    var addr = patron.mailing_address() || 
+               patron.billing_address() || 
+               patron.addresses()[0];
+
+    if (addr) {
+
+        dojo.byId('oils-selfck-user-address').innerHTML = 
+            dojo.string.substitute(localeStrings.ADDRESS, [
+                addr.street1(),
+                addr.street2() || '',
+                addr.city(),
+                addr.state(),
+                addr.post_code()
+            ]);
+
+    } else {
+        dojo.byId('oils-selfck-user-address').innerHTML = '';
+    }
+}
+
+SelfCheckManager.prototype.handleAlert = function(message, shouldPopup, sound) {
+
+    console.log("Handling alert " + message);
+
+    dojo.byId('oils-selfck-status-div').innerHTML = message;
+
+    if(shouldPopup)
+        openils.Util.addCSSClass( dojo.byId('oils-selfck-status-div'), 'checkout_failure' );
+    else
+        openils.Util.removeCSSClass( dojo.byId('oils-selfck-status-div'), 'checkout_failure' );
+
+    if(shouldPopup && this.orgSettings[SET_ALERT_POPUP]) 
+        alert(message);
+
+    if(sound && this.orgSettings[SET_ALERT_SOUND])
+        openils.Util.playAudioUrl(SelfCheckManager.audioConfig[sound]);
+}
+
+
+/**
+ * Manages the main input box
+ * @param msg The context message to display with the box
+ * @param clearOnly Don't update the context message, just clear the value and re-focus
+ * @param handler Optional "on-enter" handler.  
+ */
+SelfCheckManager.prototype.updateScanBox = function(args) {
+    args = args || {};
+
+    if(args.select) {
+        selfckScanBox.domNode.select();
+    } else {
+        selfckScanBox.attr('value', '');
+    }
+
+    if(args.password) {
+        selfckScanBox.domNode.setAttribute('type', 'password');
+    } else {
+        selfckScanBox.domNode.setAttribute('type', '');
+    }
+
+    if(args.value)
+        selfckScanBox.attr('value', args.value);
+
+    if(args.msg) 
+        dojo.byId('oils-selfck-scan-text').innerHTML = args.msg;
+
+    if(selfckScanBox._lastHandler && (args.handler || args.clearHandler)) {
+        dojo.disconnect(selfckScanBox._lastHandler);
+    }
+
+    if(args.handler) {
+        selfckScanBox._lastHandler = dojo.connect(
+            selfckScanBox, 
+            'onKeyDown', 
+            function(e) {
+                if(e.keyCode != dojo.keys.ENTER) 
+                    return;
+                args.handler(selfckScanBox.attr('value'));
+            }
+        );
+    }
+
+    selfckScanBox.focus();
+}
+
+/**
+ *  Sets up the checkout/renewal interface
+ */
+SelfCheckManager.prototype.drawCircPage = function() {
+
+    openils.Util.show('oils-selfck-circ-tbody', 'table-row-group');
+    this.goToTab('checkout');
+
+    while(this.itemsOutTbody.childNodes[0])
+        this.itemsOutTbody.removeChild(this.itemsOutTbody.childNodes[0]);
+
+    var self = this;
+    this.updateScanBox({
+        msg : dojo.string.substitute(localeStrings.CHECKOUT_PROMPT),
+        handler : function(barcode) { self.checkout(barcode); }
+    });
+
+    if(!this.circTemplate)
+        this.circTemplate = this.circTbody.removeChild(dojo.byId('oils-selfck-circ-row'));
+
+    // fines summary
+    this.updateFinesSummary();
+
+    // holds summary
+    this.updateHoldsSummary();
+
+    // items out summary
+    this.updateCircSummary();
+
+    // render mock checkouts for debugging?
+    if(this.mockCheckouts) {
+        for(var i in [1,2,3]) 
+            this.displayCheckout(this.mockCheckout, 'checkout');
+    }
+}
+
+/**
+ * Sets up the checkin page
+ */
+SelfCheckManager.prototype.drawCheckinPage = function() {
+    openils.Util.show('oils-selfck-checkin-tbody', 'table-row-group');
+    this.goToTab('checkin');
+
+    while(this.itemsCheckinTbody.childNodes[0])
+        this.itemsCheckinTbody.removeChild(
+            this.itemsCheckinTbody.childNodes[0]);
+
+    if(!this.checkinTemplate) {
+        this.checkinTemplate = 
+            this.checkinTbody.removeChild(
+                dojo.byId('oils-selfck-checkin-row'));
+    }
+
+    var self = this;
+    this.updateScanBox({
+        msg : dojo.string.substitute(localeStrings.CHECKIN_PROMPT),
+        handler : function(barcode) { self.checkin(barcode); }
+    });
+};
+
+
+SelfCheckManager.prototype.checkin = function(barcode) {
+    var self = this;
+
+    // clear the box now so checkins can continue
+    this.updateScanBox();
+
+    var backdate = checkinBackdateInput.attr('value') || null;
+    if (backdate) backdate = dojo.date.stamp.toISOString(backdate);
+
+    var row = this.checkinTemplate.cloneNode(true);
+    this.byName(row, 'barcode').innerHTML = barcode;
+
+    // put new circs at the top of the list
+    var tbody = this.checkinTbody;
+    tbody.insertBefore(row, tbody.getElementsByTagName('tr')[0]);
+
+    this.checkinCopy({
+        barcode : barcode, 
+        void_overdues : dojo.byId('oils-selfchk-amnesty-mode').checked,
+        backdate : backdate,
+        onload : function(evts) {
+            if (!evts.length) evts = [evts];
+            dojo.forEach(evts, 
+                function(evt) {
+                    self.handleCheckinResult(row, barcode, evt);
+                }
+            );
+        }
+    });
+};
+
+
+SelfCheckManager.prototype.updateFinesSummary = function() {
+    var self = this; 
+
+    // fines summary
+    fieldmapper.standardRequest(
+        ['open-ils.actor', 'open-ils.actor.user.fines.summary'],
+        {   async : true,
+            params : [this.authtoken, this.patron.id()],
+            oncomplete : function(r) {
+
+                var summary = openils.Util.readResponse(r);
+
+                dojo.byId('oils-selfck-fines-total').innerHTML = 
+                    dojo.string.substitute(
+                        localeStrings.TOTAL_FINES_ACCOUNT, 
+                        [summary.balance_owed()]
+                    );
+
+                self.creditPayableBalance = summary.balance_owed();
+            }
+        }
+    );
+}
+
+
+SelfCheckManager.prototype.drawItemsOutPage = function() {
+    openils.Util.hide('oils-selfck-circ-tbody');
+
+    this.goToTab('items_out');
+
+    while(this.itemsOutTbody.childNodes[0])
+        this.itemsOutTbody.removeChild(this.itemsOutTbody.childNodes[0]);
+
+    progressDialog.show(true);
+    
+    var self = this;
+    fieldmapper.standardRequest(
+        ['open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic'],
+        {
+            async : true,
+            params : [this.authtoken, this.patron.id()],
+            oncomplete : function(r) {
+
+                var resp = openils.Util.readResponse(r);
+
+                var circs = resp.sort(
+                    function(a, b) {
+                        if(a.circ.due_date() > b.circ.due_date())
+                            return -1;
+                        return 1;
+                    }
+                );
+
+                progressDialog.hide();
+
+                self.itemsOut = [];
+                dojo.forEach(circs,
+                    function(circ) {
+                        self.itemsOut.push(circ.circ.id());
+                        self.displayCheckout(
+                            {payload : circ}, 
+                            (circ.circ.parent_circ()) ? 'renew' : 'checkout',
+                            true
+                        );
+                    }
+                );
+            }
+        }
+    );
+}
+
+
+SelfCheckManager.prototype.goToTab = function(name) {
+    this.tabName = name;
+
+    openils.Util.hide('oils-selfck-fines-page');
+    openils.Util.hide('oils-selfck-payment-page');
+    openils.Util.hide('oils-selfck-holds-page');
+    openils.Util.hide('oils-selfck-circ-page');
+    openils.Util.hide('oils-selfck-checkin-page');
+    openils.Util.hide('oils-selfck-pay-fines-link');
+
+    dojo.removeClass('oils-selfck-nav-home', 'selected'); 
+    dojo.removeClass('oils-selfck-nav-checkin', 'selected'); 
+
+
+    switch(name) {
+        case 'checkout':
+            openils.Util.show('oils-selfck-circ-page');
+            dojo.addClass('oils-selfck-nav-home', 'selected'); 
+            break;
+        case 'checkin':
+            openils.Util.show('oils-selfck-checkin-page');
+            dojo.addClass('oils-selfck-nav-checkin', 'selected'); 
+            break;
+        case 'items_out':
+            openils.Util.show('oils-selfck-circ-page');
+            break;
+        case 'holds':
+            openils.Util.show('oils-selfck-holds-page');
+            break;
+        case 'fines':
+            openils.Util.show('oils-selfck-fines-page');
+            break;
+        case 'payment':
+            openils.Util.show('oils-selfck-payment-page');
+            break;
+    }
+}
+
+
+SelfCheckManager.prototype.printList = function() {
+    switch(this.tabName) {
+        case 'checkout':
+            this.printSessionReceipt();
+            break;
+        case 'items_out':
+            this.printItemsOutReceipt();
+            break;
+        case 'holds':
+            this.printHoldsReceipt();
+            break;
+        case 'fines':
+            this.printFinesReceipt();
+            break;
+    }
+}
+
+SelfCheckManager.prototype.updateHoldsSummary = function() {
+
+    if(!this.holdsSummary) {
+        var summary = fieldmapper.standardRequest(
+            ['open-ils.circ', 'open-ils.circ.holds.user_summary'],
+            {params : [this.authtoken, this.patron.id()]}
+        );
+
+        this.holdsSummary = {};
+        this.holdsSummary.ready = Number(summary['4']);
+        this.holdsSummary.total = 0;
+
+        for(var i in summary) 
+            this.holdsSummary.total += Number(summary[i]);
+    }
+
+    dojo.byId('oils-selfck-holds-total').innerHTML = 
+        dojo.string.substitute(
+            localeStrings.TOTAL_HOLDS, 
+            [this.holdsSummary.total]
+        );
+
+    dojo.byId('oils-selfck-holds-ready').innerHTML = 
+        dojo.string.substitute(
+            localeStrings.HOLDS_READY_FOR_PICKUP, 
+            [this.holdsSummary.ready]
+        );
+}
+
+
+SelfCheckManager.prototype.updateCircSummary = function(increment) {
+
+    if(!this.circSummary) {
+
+        var summary = fieldmapper.standardRequest(
+            ['open-ils.actor', 'open-ils.actor.user.checked_out.count'],
+            {params : [this.authtoken, this.patron.id()]}
+        );
+
+        this.circSummary = {
+            total : Number(summary.out) + Number(summary.overdue),
+            overdue : Number(summary.overdue),
+            session : 0
+        };
+    }
+
+    if(increment) {
+        // local checkout occurred.  Add to the total and the session.
+        this.circSummary.total += increment;
+        this.circSummary.session += 1;
+    }
+
+    dojo.byId('oils-selfck-circ-account-total').innerHTML = 
+        dojo.string.substitute(
+            localeStrings.TOTAL_ITEMS_ACCOUNT, 
+            [this.circSummary.total]
+        );
+
+    dojo.byId('oils-selfck-circ-session-total').innerHTML = 
+        dojo.string.substitute(
+            localeStrings.TOTAL_ITEMS_SESSION, 
+            [this.circSummary.session]
+        );
+}
+
+
+SelfCheckManager.prototype.drawHoldsPage = function() {
+
+    // TODO add option to hid scanBox
+    // this.updateScanBox(...)
+
+    this.goToTab('holds');
+
+    this.holdTbody = dojo.byId('oils-selfck-hold-tbody');
+    if(!this.holdTemplate)
+        this.holdTemplate = this.holdTbody.removeChild(dojo.byId('oils-selfck-hold-row'));
+    while(this.holdTbody.childNodes[0])
+        this.holdTbody.removeChild(this.holdTbody.childNodes[0]);
+
+    progressDialog.show(true);
+
+    var self = this;
+    fieldmapper.standardRequest( // fetch the hold IDs
+
+        ['open-ils.circ', 'open-ils.circ.holds.id_list.retrieve'],
+        {   async : true,
+            params : [this.authtoken, this.patron.id()],
+
+            oncomplete : function(r) { 
+                var ids = openils.Util.readResponse(r);
+                if(!ids || ids.length == 0) {
+                    progressDialog.hide();
+                    return;
+                }
+
+                fieldmapper.standardRequest( // fetch the hold objects with fleshed details
+                    ['open-ils.circ', 'open-ils.circ.hold.details.batch.retrieve'],
+                    {   async : true,
+                        params : [self.authtoken, ids],
+                        onresponse : function(rr) {
+                            progressDialog.hide();
+                            self.insertHold(openils.Util.readResponse(rr));
+                        }
+                    }
+                );
+            }
+        }
+    );
+}
+
+SelfCheckManager.prototype.insertHold = function(data) {
+    var row = this.holdTemplate.cloneNode(true);
+
+    if(data.mvr.isbn()) {
+        this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + data.mvr.isbn());
+    }
+
+    this.byName(row, 'title').innerHTML = data.mvr.title();
+    this.byName(row, 'author').innerHTML = data.mvr.author();
+
+    if(data.status == 4) {
+
+        // hold is ready for pickup
+        this.byName(row, 'status').innerHTML = localeStrings.HOLD_STATUS_READY;
+
+    } else {
+
+        // hold is still pending
+        this.byName(row, 'status').innerHTML = 
+            dojo.string.substitute(
+                localeStrings.HOLD_STATUS_WAITING,
+                [data.queue_position, data.potential_copies]
+            );
+    }
+
+    // find the correct place the table to slot in the hold based on queue position
+
+    var position = (data.status == 4) ? 0 : data.queue_position;
+    row.setAttribute('position', position);
+
+    for(var i = 0; i < this.holdTbody.childNodes.length; i++) {
+        var node = this.holdTbody.childNodes[i];
+        if(Number(node.getAttribute('position')) >= position) {
+            this.holdTbody.insertBefore(row, node);
+            return;
+        }
+    }
+
+    this.holdTbody.appendChild(row);
+}
+
+
+SelfCheckManager.prototype.drawFinesPage = function() {
+
+    // TODO add option to hid scanBox
+    // this.updateScanBox(...)
+
+    this.goToTab('fines');
+    progressDialog.show(true);
+
+    if(this.creditPayableBalance > 0 && this.orgSettings[SET_CC_PAYMENT_ALLOWED]) {
+        openils.Util.show('oils-selfck-pay-fines-link', 'inline');
+    }
+
+    this.finesTbody = dojo.byId('oils-selfck-fines-tbody');
+    if(!this.finesTemplate)
+        this.finesTemplate = this.finesTbody.removeChild(dojo.byId('oils-selfck-fines-row'));
+    while(this.finesTbody.childNodes[0])
+        this.finesTbody.removeChild(this.finesTbody.childNodes[0]);
+
+    // when user clicks on a selector checkbox, update the total owed
+    var updateSelected = function() {
+        var total = 0;
+        dojo.forEach(
+            dojo.query('[name=selector]', this.finesTbody),
+            function(input) {
+                if(input.checked)
+                    total += Number(input.getAttribute('balance_owed'));
+            }
+        );
+
+        total = total.toFixed(2);
+        dojo.byId('oils-selfck-selected-total').innerHTML = 
+            dojo.string.substitute(localeStrings.TOTAL_FINES_SELECTED, [total]);
+    }
+
+    // wire up the batch on/off selector
+    var sel = dojo.byId('oils-selfck-fines-selector');
+    sel.onchange = function() {
+        dojo.forEach(
+            dojo.query('[name=selector]', this.finesTbody),
+            function(input) {
+                input.checked = sel.checked;
+            }
+        );
+    };
+
+    var self = this;
+    var handler = function(dataList) {
+
+        self.finesCount = dataList.length;
+        self.finesData = dataList;
+
+        for(var i in dataList) {
+
+            var data = dataList[i];
+            var row = self.finesTemplate.cloneNode(true);
+            var type = data.transaction.xact_type();
+
+            if(type == 'circulation') {
+                self.byName(row, 'type').innerHTML = type;
+                self.byName(row, 'details').innerHTML = data.record.title();
+
+            } else if(type == 'grocery') {
+                self.byName(row, 'type').innerHTML = 'Miscellaneous'; // Go ahead and head off any confusion around "grocery".  TODO i18n
+                self.byName(row, 'details').innerHTML = data.transaction.last_billing_type();
+            }
+
+            self.byName(row, 'total_owed').innerHTML = data.transaction.total_owed();
+            self.byName(row, 'total_paid').innerHTML = data.transaction.total_paid();
+            self.byName(row, 'balance').innerHTML = data.transaction.balance_owed();
+
+            // row selector
+            var selector = self.byName(row, 'selector')
+            selector.onchange = updateSelected;
+            selector.setAttribute('xact', data.transaction.id());
+            selector.setAttribute('balance_owed', data.transaction.balance_owed());
+            selector.checked = true;
+
+            self.finesTbody.appendChild(row);
+        }
+
+        updateSelected();
+    }
+
+
+    fieldmapper.standardRequest( 
+        ['open-ils.actor', 'open-ils.actor.user.transactions.have_balance.fleshed'],
+        {   async : true,
+            params : [this.authtoken, this.patron.id()],
+            oncomplete : function(r) { 
+                progressDialog.hide();
+                handler(openils.Util.readResponse(r));
+            }
+        }
+    );
+}
+
+/** top-level checkin handler */
+SelfCheckManager.prototype.checkinCopy = function(args) {
+    fieldmapper.standardRequest(
+        ['open-ils.circ', 'open-ils.circ.checkin.override'],
+        {   async : true,
+            params : [
+                this.authtoken, {
+                    copy_barcode : args.barcode,
+                    backdate : args.backdate,
+                    void_overdues : args.void_overdues
+                }
+            ],
+            oncomplete : function(r) {
+                var resp = openils.Util.readResponse(r, true);
+                args.onload(resp);
+            }
+        }
+    );
+};
+
+/** used for checkins required to fullfil a checkout */
+SelfCheckManager.prototype.inlineCheckinCopy = function(barcode, abortTransit) {
+
+    if (abortTransit) {
+        var resp = fieldmapper.standardRequest(
+            ['open-ils.circ', 'open-ils.circ.transit.abort'],
+            {params : [this.authtoken, {barcode : barcode}]}
+        );
+    
+        // resp == 1 on success
+        if(openils.Event.parse(resp))
+            return false;
+    }
+
+    var resp = fieldmapper.standardRequest(
+        ['open-ils.circ', 'open-ils.circ.checkin.override'],
+        {params : [
+            this.authtoken, {
+                patron_id : this.patron.id(),
+                copy_barcode : barcode,
+                noop : true
+            }
+        ]}
+    );
+
+    if(!resp.length) resp = [resp];
+    for(var i = 0; i < resp.length; i++) {
+        var tc = openils.Event.parse(resp[i]).textcode;
+        if(tc == 'SUCCESS' || tc == 'NO_CHANGE') {
+            continue;
+        } else {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+/**
+ * Check out a single item.  If the item is already checked 
+ * out to the patron, redirect to renew()
+ */
+SelfCheckManager.prototype.checkout = function(barcode, override) {
+
+    this.prevCirc = null;
+
+    if(!barcode) {
+        this.updateScanbox(null, true);
+        return;
+    }
+
+    if(this.mockCheckouts) {
+        // if we're in mock-checkout mode, just insert another
+        // fake circ into the table and get out of here.
+        this.displayCheckout(this.mockCheckout, 'checkout');
+        return;
+    }
+
+    // TODO see if it's a patron barcode
+    // TODO see if this item has already been checked out in this session
+
+    var method = 'open-ils.circ.checkout.full';
+    if(override) method += '.override';
+
+    console.log("Checkout out item " + barcode + " with method " + method);
+
+    var result = fieldmapper.standardRequest(
+        ['open-ils.circ', method],
+        {params: [
+            this.authtoken, {
+                patron_id : this.patron.id(),
+                copy_barcode : barcode
+            }
+        ]}
+    );
+
+    var stat = this.handleXactResult('checkout', barcode, result);
+
+    if(stat.override) {
+        this.checkout(barcode, true);
+    } else if(stat.doOver) {
+        this.checkout(barcode);
+    } else if(stat.renew) {
+        this.renew(barcode);
+    }
+}
+
+SelfCheckManager.prototype.failPartMessage = function(result) {
+    if (result.payload && result.payload.fail_part) {
+        var stringKey = "FAIL_PART_" +
+            result.payload.fail_part.replace(/\./g, "_");
+        return localeStrings[stringKey];
+    } else {
+        return null;
+    }
+}
+
+SelfCheckManager.prototype.displayCheckin = function(row, result, outcomeText) {
+    console.log('display checkin results ' + result);
+
+    var copy = result.payload.copy;
+    var record = result.payload.record;
+    var circ = result.payload.circ;
+
+    if(record.isbn()) {
+        this.byName(row, 'jacket').setAttribute('src', 
+            '/opac/extras/ac/jacket/small/' + record.isbn());
+    }
+
+    this.byName(row, 'barcode').innerHTML = copy.barcode();
+    this.byName(row, 'title').innerHTML = record.title();
+    this.byName(row, 'author').innerHTML = record.author();
+    this.byName(row, 'status').innerHTML = this.copyStatusMap[copy.status()].name();
+    this.byName(row, 'outcome').innerHTML = outcomeText || result.textcode;
+
+    if (circ) {
+        var date = dojo.date.stamp.fromISOString(circ.due_date());
+        this.byName(row, 'due_date').innerHTML = 
+            dojo.date.locale.format(date, {selector : 'date'});
+
+        // if a patron is loaded and we just checked an item
+        // in for this patron, decrement the items-out count by 1
+        if (this.patron && this.patron.id() == circ.usr())
+            this.updateCircSummary(-1);
+    }
+}
+
+SelfCheckManager.prototype.handleCheckinResult = function(row, item, result) {
+    var displayText = '';
+    var popup = false;  
+    var sound = ''; // sound file reference
+    var payload = result.payload || {};
+    var tc = result.textcode;
+
+    console.log('checkin resulted in ' + tc);
+
+    if (tc == 'NO_SESSION') {
+
+        return this.logoutStaff();
+
+    } else if (tc == 'SUCCESS') {
+
+        displayText = dojo.string.substitute(
+            localeStrings.CHECKIN_SUCCESS, [item]);
+        this.displayCheckin(row, result);
+
+    } else if (tc == 'NO_CHANGE') {
+
+        displayText = dojo.string.substitute(
+            localeStrings.CHECKIN_NO_CHANGE, [item]);
+        this.displayCheckin(row, result);
+
+    } else if (tc == 'ROUTE_ITEM') {
+
+        var outcomeText;
+        if (result.source.org) {
+            var sn = fieldmapper.aou.findOrgUnit(result.source.org).shortname();
+            displayText = dojo.string.substitute(
+                localeStrings.CHECKIN_ROUTE_ITEM, [item, sn]);
+            outcomeText = tc + ' => ' + sn;
+        }
+
+        this.displayCheckin(row, result, outcomeText);
+
+    } else if (tc == 'ASSET_COPY_NOT_FOUND') {
+
+        // remove the in-progress row
+        row.parentNode.removeChild(row);
+
+        displayText = dojo.string.substitute(
+            localeStrings.ITEM_NOT_CATALOGED, [item]);
+
+    } else {
+
+        // remove the in-progress row
+        row.parentNode.removeChild(row);
+
+        displayText = dojo.string.substitute(
+            localeStrings.UNKNOWN_ERROR, [tc]);
+    }
+
+    this.handleAlert(displayText, popup, sound);
+    return {};
+}
+
+SelfCheckManager.prototype.handleXactResult = function(action, item, result) {
+
+    var displayText = '';
+
+    // If true, the display message is important enough to pop up.  Whether or not
+    // an alert() actually occurs, depends on org unit settings
+    var popup = false;  
+    var sound = ''; // sound file reference
+    var payload = result.payload || {};
+    var overrideEvents = this.orgSettings[SET_AUTO_OVERRIDE_EVENTS];
+    var blockStatuses = this.orgSettings[SET_BLOCK_CHECKOUT_ON_COPY_STATUS];
+        
+    if(result.textcode == 'NO_SESSION') {
+
+        return this.logoutStaff();
+
+    } else if(result.textcode == 'SUCCESS') {
+
+        if(action == 'checkout') {
+
+            displayText = dojo.string.substitute(localeStrings.CHECKOUT_SUCCESS, [item]);
+            this.displayCheckout(result, 'checkout');
+
+            if(payload.holds_fulfilled && payload.holds_fulfilled.length) {
+                // A hold was fulfilled, update the hold numbers in the circ summary
+                console.log("fulfilled hold " + payload.holds_fulfilled + " during checkout");
+                this.holdsSummary = null;
+                this.updateHoldsSummary();
+            }
+
+            this.updateCircSummary(1);
+
+        } else if(action == 'renew') {
+
+            displayText = dojo.string.substitute(localeStrings.RENEW_SUCCESS, [item]);
+            this.displayCheckout(result, 'renew');
+        }
+
+        this.checkouts.push({circ : result.payload.circ.id()});
+        sound = 'checkout-success';
+        this.updateScanBox();
+
+    } else if(result.textcode == 'OPEN_CIRCULATION_EXISTS' && action == 'checkout') {
+
+        // Server says the item is already checked out.  If it's checked out to the
+        // current user, we may need to renew it.  
+
+        if(payload.old_circ) { 
+
+            /*
+            old_circ refers to the previous checkout IFF it's for the same user. 
+            If no auto-renew interval is not defined, assume we should renew it
+            If an auto-renew interval is defined and the payload comes back with
+            auto_renew set to true, do the renewal.  Otherwise, let the patron know
+            the item is already checked out to them.  */
+
+            if( !this.orgSettings[SET_AUTO_RENEW_INTERVAL] ||
+                (this.orgSettings[SET_AUTO_RENEW_INTERVAL] && payload.auto_renew) ) {
+                this.prevCirc = payload.old_circ.id();
+                return { renew : true };
+            }
+
+            popup = true;
+            sound = 'checkout-failure';
+            displayText = dojo.string.substitute(localeStrings.ALREADY_OUT, [item]);
+
+        } else {
+
+            if( // copy is marked lost.  if configured to do so, check it in and try again.
+                result.payload.copy && 
+                result.payload.copy.status() == /* LOST */ 3 &&
+                overrideEvents && overrideEvents.length &&
+                overrideEvents.indexOf('COPY_STATUS_LOST') != -1) {
+
+                    if(this.inlineCheckinCopy(item)) {
+                        return { doOver : true };
+                    }
+            }
+
+            
+            // item is checked out to some other user
+            popup = true;
+            sound = 'checkout-failure';
+            displayText = dojo.string.substitute(localeStrings.OPEN_CIRCULATION_EXISTS, [item]);
+        }
+
+        this.updateScanBox({select:true});
+
+    } else {
+
+    
+        if(overrideEvents && overrideEvents.length) {
+            
+            // see if the events we received are all in the list of
+            // events to override
+    
+            if(!result.length) result = [result];
+    
+            var override = true;
+            for(var i = 0; i < result.length; i++) {
+
+                var match = overrideEvents.filter(function(e) { return (e == result[i].textcode); })[0];
+
+                if(!match) {
+                    override = false;
+                    break;
+                }
+
+                if(result[i].textcode == 'COPY_NOT_AVAILABLE' && blockStatuses && blockStatuses.length) {
+
+                    var stat = result[i].payload.status(); // copy status
+                    if(typeof stat == 'object') stat = stat.id();
+
+                    var match2 = blockStatuses.filter(function(e) { return (e == stat); })[0];
+
+                    if(match2) { // copy is in a blocked status
+                        override = false;
+                        break;
+                    }
+                }
+
+                if(result[i].textcode == 'COPY_IN_TRANSIT') {
+                    // to override a transit, we have to abort the transit and check it in first
+                    if(this.inlineCheckinCopy(item, true)) {
+                        return { doOver : true };
+                    } else {
+                        override = false;
+                    }
+                }
+            }
+
+            if(override) 
+                return { override : true };
+        }
+    
+        this.updateScanBox({select : true});
+        popup = true;
+        sound = 'checkout-failure';
+
+        if(action == 'renew')
+            this.checkouts.push({circ : this.prevCirc, renewal_failure : true});
+
+        if(result.length) 
+            result = result[0];
+
+        switch(result.textcode) {
+
+            // TODO custom handler for blocking penalties
+
+            case 'MAX_RENEWALS_REACHED' :
+                displayText = dojo.string.substitute(
+                    localeStrings.MAX_RENEWALS, [item]);
+                break;
+
+            case 'ITEM_NOT_CATALOGED' :
+                displayText = dojo.string.substitute(
+                    localeStrings.ITEM_NOT_CATALOGED, [item]);
+                break;
+
+            case 'OPEN_CIRCULATION_EXISTS' :
+                displayText = dojo.string.substitute(
+                    localeStrings.OPEN_CIRCULATION_EXISTS, [item]);
+
+                break;
+
+            default:
+                console.error('Unhandled event ' + result.textcode);
+
+                if (!(displayText = this.failPartMessage(result))) {
+                    if (action == 'checkout' || action == 'renew') {
+                        displayText = dojo.string.substitute(
+                            localeStrings.GENERIC_CIRC_FAILURE, [item]);
+                    } else {
+                        displayText = dojo.string.substitute(
+                            localeStrings.UNKNOWN_ERROR, [result.textcode]);
+                    }
+                }
+        }
+    }
+
+    this.handleAlert(displayText, popup, sound);
+    return {};
+}
+
+
+/**
+ * Renew an item
+ */
+SelfCheckManager.prototype.renew = function(barcode, override) {
+
+    var method = 'open-ils.circ.renew';
+    if(override) method += '.override';
+
+    console.log("Renewing item " + barcode + " with method " + method);
+
+    var result = fieldmapper.standardRequest(
+        ['open-ils.circ', method],
+        {params: [
+            this.authtoken, {
+                patron_id : this.patron.id(),
+                copy_barcode : barcode
+            }
+        ]}
+    );
+
+    console.log(js2JSON(result));
+
+    var stat = this.handleXactResult('renew', barcode, result);
+
+    if(stat.override)
+        this.renew(barcode, true);
+}
+
+/**
+ * Display the result of a checkout or renewal in the items out table
+ */
+SelfCheckManager.prototype.displayCheckout = function(evt, type, itemsOut) {
+
+    var copy = evt.payload.copy;
+    var record = evt.payload.record;
+    var circ = evt.payload.circ;
+    var row = this.circTemplate.cloneNode(true);
+
+    if(record.isbn()) {
+        this.byName(row, 'jacket').setAttribute('src', '/opac/extras/ac/jacket/small/' + record.isbn());
+    }
+
+    this.byName(row, 'barcode').innerHTML = copy.barcode();
+    this.byName(row, 'title').innerHTML = record.title();
+    this.byName(row, 'author').innerHTML = record.author();
+    this.byName(row, 'remaining').innerHTML = circ.renewal_remaining();
+    openils.Util.show(this.byName(row, type));
+
+    var date = dojo.date.stamp.fromISOString(circ.due_date());
+    this.byName(row, 'due_date').innerHTML = 
+        dojo.date.locale.format(date, {selector : 'date'});
+
+    // put new circs at the top of the list
+    var tbody = this.circTbody;
+    if(itemsOut) tbody = this.itemsOutTbody;
+    tbody.insertBefore(row, tbody.getElementsByTagName('tr')[0]);
+}
+
+
+SelfCheckManager.prototype.byName = function(node, name) {
+    return dojo.query('[name=' + name+']', node)[0];
+}
+
+
+SelfCheckManager.prototype.initPrinter = function() {
+    try { // Mozilla only
+               netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
+        netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+        netscape.security.PrivilegeManager.enablePrivilege('UniversalPreferencesRead');
+        netscape.security.PrivilegeManager.enablePrivilege('UniversalPreferencesWrite');
+        var pref = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+        if (pref)
+            pref.setBoolPref('print.always_print_silent', true);
+    } catch(E) {
+        console.log("Unable to initialize auto-printing"); 
+    }
+}
+
+/**
+ * Print a receipt for this session's checkouts
+ */
+SelfCheckManager.prototype.printSessionReceipt = function(callback) {
+
+    var circIds = [];
+    var circCtx = []; // circ context data.  in this case, renewal_failure info
+
+    // collect the circs and failure info
+    dojo.forEach(
+        this.checkouts, 
+        function(blob) {
+            circIds.push(blob.circ);
+            circCtx.push({renewal_failure:blob.renewal_failure});
+        }
+    );
+
+    var params = [
+        this.authtoken, 
+        this.staff.ws_ou(),
+        null,
+        'format.selfcheck.checkout',
+        'print-on-demand',
+        circIds,
+        circCtx
+    ];
+
+    var self = this;
+    fieldmapper.standardRequest(
+        ['open-ils.circ', 'open-ils.circ.fire_circ_trigger_events'],
+        {   
+            async : true,
+            params : params,
+            oncomplete : function(r) {
+                var resp = openils.Util.readResponse(r);
+                var output = resp.template_output();
+                if(output) {
+                    self.printData(output.data(), self.checkouts.length, callback); 
+                } else {
+                    var error = resp.error_output();
+                    if(error) {
+                        throw new Error("Error creating receipt: " + error.data());
+                    } else {
+                        throw new Error("No receipt data returned from server");
+                    }
+                }
+            }
+        }
+    );
+}
+
+SelfCheckManager.prototype.printData = function(data, numItems, callback) {
+
+    var win = window.open('', '', 'resizable,width=700,height=500,scrollbars=1,chrome'); 
+    win.document.body.innerHTML = data;
+    win.print();
+
+    /*
+     * There is no way to know when the browser is done printing.
+     * Make a best guess at when to close the print window by basing
+     * the setTimeout wait on the number of items to be printed plus
+     * a small buffer
+     */
+    var sleepTime = 1000;
+    if(numItems > 0) 
+        sleepTime += (numItems / 2) * 1000;
+
+    setTimeout(
+        function() { 
+            win.close(); // close the print window
+            if(callback)
+                callback(); // fire optional post-print callback
+        },
+        sleepTime 
+    );
+}
+
+
+/**
+ * Print a receipt for this user's items out
+ */
+SelfCheckManager.prototype.printItemsOutReceipt = function(callback) {
+
+    if(!this.itemsOut.length) return;
+
+    progressDialog.show(true);
+
+    var params = [
+        this.authtoken, 
+        this.staff.ws_ou(),
+        null,
+        'format.selfcheck.items_out',
+        'print-on-demand',
+        this.itemsOut
+    ];
+
+    var self = this;
+    fieldmapper.standardRequest(
+        ['open-ils.circ', 'open-ils.circ.fire_circ_trigger_events'],
+        {   
+            async : true,
+            params : params,
+            oncomplete : function(r) {
+                progressDialog.hide();
+                var resp = openils.Util.readResponse(r);
+                var output = resp.template_output();
+                if(output) {
+                    self.printData(output.data(), self.itemsOut.length, callback); 
+                } else {
+                    var error = resp.error_output();
+                    if(error) {
+                        throw new Error("Error creating receipt: " + error.data());
+                    } else {
+                        throw new Error("No receipt data returned from server");
+                    }
+                }
+            }
+        }
+    );
+}
+
+/**
+ * Print a receipt for this user's items out
+ */
+SelfCheckManager.prototype.printHoldsReceipt = function(callback) {
+
+    if(!this.holds.length) return;
+
+    progressDialog.show(true);
+
+    var holdIds = [];
+    var holdData = [];
+
+    dojo.forEach(this.holds,
+        function(data) {
+            holdIds.push(data.hold.id());
+            if(data.status == 4) {
+                holdData.push({ready : true});
+            } else {
+                holdData.push({
+                    queue_position : data.queue_position, 
+                    potential_copies : data.potential_copies
+                });
+            }
+        }
+    );
+
+    var params = [
+        this.authtoken, 
+        this.staff.ws_ou(),
+        null,
+        'format.selfcheck.holds',
+        'print-on-demand',
+        holdIds,
+        holdData
+    ];
+
+    var self = this;
+    fieldmapper.standardRequest(
+        ['open-ils.circ', 'open-ils.circ.fire_hold_trigger_events'],
+        {   
+            async : true,
+            params : params,
+            oncomplete : function(r) {
+                progressDialog.hide();
+                var resp = openils.Util.readResponse(r);
+                var output = resp.template_output();
+                if(output) {
+                    self.printData(output.data(), self.holds.length, callback); 
+                } else {
+                    var error = resp.error_output();
+                    if(error) {
+                        throw new Error("Error creating receipt: " + error.data());
+                    } else {
+                        throw new Error("No receipt data returned from server");
+                    }
+                }
+            }
+        }
+    );
+}
+
+
+SelfCheckManager.prototype.printPaymentReceipt = function(response, callback) {
+    
+    var self = this;
+    progressDialog.show(true);
+
+    fieldmapper.standardRequest(
+        ['open-ils.circ', 'open-ils.circ.money.payment_receipt.print'],
+        {
+            async : true,
+            params : [this.authtoken, response.payments],
+            oncomplete : function(r) {
+                var resp = openils.Util.readResponse(r);
+                var output = resp.template_output();
+                progressDialog.hide();
+                if(output) {
+                    self.printData(output.data(), 1, callback); 
+                } else {
+                    var error = resp.error_output();
+                    if(error) {
+                        throw new Error("Error creating receipt: " + error.data());
+                    } else {
+                        throw new Error("No receipt data returned from server");
+                    }
+                }
+            }
+        }
+    );
+}
+
+/**
+ * Print a receipt for this user's items out
+ */
+SelfCheckManager.prototype.printFinesReceipt = function(callback) {
+
+    progressDialog.show(true);
+
+    var params = [
+        this.authtoken, 
+        this.staff.ws_ou(),
+        null,
+        'format.selfcheck.fines',
+        'print-on-demand',
+        [this.patron.id()]
+    ];
+
+    var self = this;
+    fieldmapper.standardRequest(
+        ['open-ils.circ', 'open-ils.circ.fire_user_trigger_events'],
+        {   
+            async : true,
+            params : params,
+            oncomplete : function(r) {
+                progressDialog.hide();
+                var resp = openils.Util.readResponse(r);
+                var output = resp.template_output();
+                if(output) {
+                    self.printData(output.data(), self.finesCount, callback); 
+                } else {
+                    var error = resp.error_output();
+                    if(error) {
+                        throw new Error("Error creating receipt: " + error.data());
+                    } else {
+                        throw new Error("No receipt data returned from server");
+                    }
+                }
+            }
+        }
+    );
+}
+
+
+
+
+/**
+ * Logout the patron and return to the login page
+ */
+SelfCheckManager.prototype.logoutPatron = function(print) {
+    progressDialog.show(true); // prevent patron from clicking logout link twice
+    if(print && this.checkouts.length) {
+        this.printSessionReceipt(
+            function() {
+                location.href = location.href;
+            }
+        );
+    } else {
+        location.href = location.href;
+    }
+}
+
+
+/**
+ * Fire up the manager on page load
+ */
+openils.Util.addOnLoad(
+    function() {
+        new SelfCheckManager().init();
+    }
+);