[open-ils-commits] [GIT] Evergreen ILS branch master updated. f1e2631e2cf586d906cf9fe0f971274045b8d243

Evergreen Git git at git.evergreen-ils.org
Wed Aug 30 09:52:32 EDT 2017


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  f1e2631e2cf586d906cf9fe0f971274045b8d243 (commit)
       via  23886b41105ee0e901a29fd113684a24ffd02852 (commit)
       via  346cba8d8f4606a3fdbc356194d5b511869cebb7 (commit)
       via  b77f4cb27f98bf0a673f0a7e9c286b2dd2362d4a (commit)
       via  40079db66be4ab3450182362b1ff5ac684a449cf (commit)
       via  2b843e55d59b31068197eb248294395af8b33602 (commit)
       via  185fbc402af561165439ddf04e82cf0dab804da7 (commit)
       via  cb523260f300377b9977b98d1bb9010e0419581d (commit)
       via  5305b78a3c63b85a770f873139aec4701077ae38 (commit)
       via  09854bc35b46dc887e49ee307d71fa6ceab9813b (commit)
       via  83dbb0001134b4323acb7e50b79c67d779aff1f5 (commit)
       via  d94719306d63457ed25c03deb2d4ce07d01e8315 (commit)
       via  7c3cdbbd140865e07d08952422c82605ac8c5676 (commit)
      from  4bea26e2721cd0cb52f2a4a6f7b570e98a83b720 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit f1e2631e2cf586d906cf9fe0f971274045b8d243
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Aug 29 15:03:51 2017 -0400

    The ngToast maintainers decided to trick us with a new directory name. Thanks.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/Gruntfile.js b/Open-ILS/web/js/ui/default/staff/Gruntfile.js
index d8af904..978e918 100644
--- a/Open-ILS/web/js/ui/default/staff/Gruntfile.js
+++ b/Open-ILS/web/js/ui/default/staff/Gruntfile.js
@@ -55,8 +55,8 @@ module.exports = function(grunt) {
           src : [
             'node_modules/angular-hotkeys/build/hotkeys.min.css',
             'node_modules/bootstrap/dist/css/bootstrap.min.css', 
-            'node_modules/ngtoast/dist/ngToast.min.css',
-            'node_modules/ngtoast/dist/ngToast-animations.min.css',
+            'node_modules/ng-toast/dist/ngToast.min.css',
+            'node_modules/ng-toast/dist/ngToast-animations.min.css',
             'node_modules/angular-tree-control/css/tree-control.css',
             'node_modules/angular-tree-control/css/tree-control-attribute.css',
             'node_modules/angular-tablesort/tablesort.css'

commit 23886b41105ee0e901a29fd113684a24ffd02852
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Aug 29 14:42:03 2017 -0400

    Fix the "404 asset" test
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/live_t/24-offline-all-assets.t b/Open-ILS/src/perlmods/live_t/24-offline-all-assets.t
index 334778f..afecee3 100644
--- a/Open-ILS/src/perlmods/live_t/24-offline-all-assets.t
+++ b/Open-ILS/src/perlmods/live_t/24-offline-all-assets.t
@@ -2,6 +2,7 @@
 
 use Test::More tests => 1;
 
-my $command = = 'wget --no-check-certificate -m https://localhost/eg/staff/offline-interface/session 2>&1 |grep -B 2 404|grep https|grep -v robots.txt|wc -l'
-is(`$command`, '0', "No missing assets required by the offline interface");
+my $command = 'wget --no-check-certificate -m https://localhost/eg/staff/offline-interface 2>&1 |grep -B 2 404|grep https|grep -v robots.txt|wc -l';
+chomp(my $output = `$command`);
+is($output, '0', "No missing assets required by the offline interface");
 

commit 346cba8d8f4606a3fdbc356194d5b511869cebb7
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Aug 29 14:15:06 2017 -0400

    Reorder the tabs and adjust the default based on logged-in-ness
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/staff/offline-interface.tt2 b/Open-ILS/src/templates/staff/offline-interface.tt2
index 0cc81fd..725b7c3 100644
--- a/Open-ILS/src/templates/staff/offline-interface.tt2
+++ b/Open-ILS/src/templates/staff/offline-interface.tt2
@@ -67,6 +67,183 @@
     <uib-tabset active="active_tab">
       <!-- note that non-numeric index values must be enclosed in single-quotes,
            otherwise selecting the active table won't work cleanly -->
+      <uib-tab ng-show="logged_in" index="'session'" heading="[% l('Session Management') %]">
+        <div class="col-md-12" ng-controller="OfflineSessionCtrl">
+          <uib-tabset active="active_session_tab">
+            <uib-tab index="'pending'" heading="[% l('Pending Transactions') %]">
+              <div class="row">
+                <div class="col-md-12 container">
+                  <button
+                    class="btn btn-default"
+                    ng-disabled="pending_xacts.length == 0"
+                    eg-line-exporter
+                    default-file-name="pending.xacts"
+                    json-array="pending_xacts"
+                  >[% l('Export Transactions') %]</button>
+                  <div class="btn-group">
+                    <span class="btn btn-default btn-file">
+                      [% l('Import Transactions') %]
+                      <input type="file" eg-file-reader container="imported_pending_xacts.data">
+                    </span>
+                  </div>
+                <button class="btn btn-warning pull-right" ng-click="clear_pending()">[% l('Clear Transactions') %]</button>
+                </div>
+              </div>
+              <div class="row">
+                <div class="col-md-12 container">
+                  <table class="table">
+                    <thead>
+                      <tr>
+                        <th>[% l('Type') %]</th>
+                        <th>[% l('Timestamp') %]</th>
+                        <th>[% l('Patron Barcode') %]</th>
+                        <th>[% l('Item Barcode') %]</th>
+                        <th>[% l('Non-cataloged Type') %]</th>
+                        <th>[% l('Checkout Date') %]</th>
+                        <th>[% l('Due Date') %]</th>
+                        <th>[% l('Checkin Date') %]</th>
+                        <th>[% l('First Name') %]</th>
+                        <th>[% l('Last Name') %]</th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      <tr ng-repeat="xact in pending_xacts track by $index">
+                        <td>{{xact.type}}</td>
+                        <td>{{createDate(xact.timestamp, true) | date:'short'}}</td>
+                        <td>{{xact.patron_barcode || xact.user.card.barcode}}</td>
+                        <td>{{xact.barcode}}</td>
+                        <td>{{lookupNoncatTypeName(xact.noncat_type)}}</td>
+                        <td>{{createDate(xact.checkout_time) | date:'short'}}</td>
+                        <td>{{createDate(xact.due_date) | date:'shortDate'}}</td>
+                        <td>{{createDate(xact.backdate) | date:'shortDate'}}</td>
+                        <td>{{xact.user.first_given_name}}</td>
+                        <td>{{xact.user.family_name}}</td>
+                      </tr>
+                    </tbody>
+                  </table>
+                </div>
+              </div>
+            </uib-tab>
+            <uib-tab index="'offline_sessions'" heading="[% l('Offline Sessions') %]">
+              <div class="row">
+                <div class="col-md-12">
+                  <button
+                    class="btn btn-primary"
+                    ng-disabled="!logged_in"
+                    ng-click="createSession()">[% l('Create Session') %]</button>
+                  <button
+                    class="btn btn-default pull-right"
+                    ng-disabled="!logged_in"
+                    ng-click="refreshSessions()">[% l('Refresh') %]</button>
+                </div>
+              </div>
+              <div class="row">
+                <div class="col-md-12"><h2>[% l('Session List') %]</h2></div>
+              </div>
+              <div class="row">
+                <div class="col-md-12">
+                  <table class="table" ts-wrapper>
+                    <thead>
+                      <tr>
+                        <th ts-criteria="org">[% l('Organization') %]</th>
+                        <th ts-criteria="creator">[% l('Created By') %]</th>
+                        <th ts-criteria="description">[% l('Description') %]</th>
+                        <th ts-criteria="create_time|parseInt" ts-default="descending">[% l('Date Created') %]</th>
+                        <th>[% l('Upload Count') %]</th>
+                        <th>[% l('Transactions Processed') %]</th>
+                        <th ts-criteria="end_time|parseInt">[% l('Date Completed') %]</th>
+                        <th></th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      <tr ts-repeat
+                        ng-repeat="ses in sessions track by $index"
+                        ng-click="setSession(ses, $index)"
+                        ng-class="{'bg-info':current_session_index==$index}"
+                      >
+                        <td>{{ses.org}}</td>
+                        <td>{{ses.creator}}</td>
+                        <td>{{ses.description}}</td>
+                        <td>{{createDate(ses.create_time, true) | date:'short'}}</td>
+                        <td>{{ses.total}}</td>
+                        <td>{{ses.num_complete}}</td>
+                        <td>{{createDate(ses.end_time, true) | date:'short'}}</td>
+                        <td>
+                          <button
+                            class="btn btn-info btn-xs"
+                            ng-disabled="!logged_in || pending_xacts.length == 0 || ses.end_time"
+                            ng-click="uploadPending(ses, $index)"
+                          >[% l('Upload') %]</button>
+                          <button
+                            class="btn btn-warning btn-xs"
+                            ng-disabled="!logged_in || ses.total == 0 || ses.end_time"
+                            ng-click="processSession(ses, $index)"
+                          >[% l('Process') %]</button>
+                        </td>
+                      </tr>
+                    </tbody>
+                  </table>
+                </div>
+              </div>
+              <div class="row">
+                    <div class="col-md-12"><hr/></div>
+              </div>
+              <div class="row">
+                    <div class="col-md-12"><h2>[% l('Exception List') %]</h2></div>
+              </div>
+              <div class="row">
+                <div class="col-md-12">
+                  <table class="table">
+                    <thead>
+                      <tr>
+                        <th>[% l('Workstation') %]</th>
+                        <th>[% l('Type') %]</th>
+                        <th>[% l('Timestamp') %]</th>
+                        <th>[% l('Event Name') %]</th>
+                        <th>[% l('Patron Barcode') %]</th>
+                        <th>[% l('Item Barcode') %]</th>
+                        <th>[% l('Non-cataloged Type') %]</th>
+                        <th>[% l('Checkout Date') %]</th>
+                        <th>[% l('Due Date') %]</th>
+                        <th>[% l('Checkin Date') %]</th>
+                        <th></th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      <tr ng-repeat="xact in current_session.exceptions track by $index">
+                        <td>{{xact.command._workstation}}</td>
+                        <td>{{xact.command.type}}</td>
+                        <td>{{createDate(xact.command.timestamp, true) | date:'short'}}</td>
+                        <td>{{xact.event.textcode}}</td>
+                        <td>{{xact.command.patron_barcode || xact.command.user.card.barcode}}</td>
+                        <td>{{xact.command.barcode}}</td>
+                        <td>{{lookupNoncatTypeName(xact.command.noncat_type)}}</td>
+                        <td>{{createDate(xact.command.checkout_time) | date:'short'}}</td>
+                        <td>{{createDate(xact.command.due_date) | date:'shortDate'}}</td>
+                        <td>{{createDate(xact.command.backdate) | date:'shortDate'}}</td>
+                        <td>
+                          <button
+                            class="btn btn-info btn-xs"
+                            ng-disabled="!logged_in || !xact.command.barcode"
+                            ng-click="retrieveItem(xact.command.barcode)">[% l('Item') %]</button>
+                          <button
+                            class="btn btn-info btn-xs"
+                            ng-disabled="!logged_in || (!xact.command.patron_barcode && xact.command.user.card.barcode)"
+                            ng-click="retrievePatron(xact.command.patron_barcode)">[% l('Patron') %]</button>
+                          <button
+                            class="btn btn-info btn-xs"
+                            ng-disabled="!logged_in"
+                            ng-click="retrieveDetails(xact)">[% l('Debug') %]</button>
+                        </td>
+                      </tr>
+                    </tbody>
+                  </table>
+                </div>
+              </div>
+            </uib-tab>
+          </uib-tabset>
+        </div>
+      </uib-tab>
       <uib-tab index="'checkout'" heading="[% l('Checkout') %]">
 
         <div class="row">
@@ -411,183 +588,6 @@
           <div>[% INCLUDE 'staff/circ/patron/t_edit.tt2' %]</div>
         </div>
       </uib-tab>
-      <uib-tab ng-if="logged_in" index="'session'" heading="[% l('Session Management') %]">
-        <div class="col-md-12" ng-controller="OfflineSessionCtrl">
-          <uib-tabset active="active_session_tab">
-            <uib-tab index="'pending'" heading="[% l('Pending Transactions') %]">
-              <div class="row">
-                <div class="col-md-12 container">
-                  <button
-                    class="btn btn-default"
-                    ng-disabled="pending_xacts.length == 0"
-                    eg-line-exporter
-                    default-file-name="pending.xacts"
-                    json-array="pending_xacts"
-                  >[% l('Export Transactions') %]</button>
-                  <div class="btn-group">
-                    <span class="btn btn-default btn-file">
-                      [% l('Import Transactions') %]
-                      <input type="file" eg-file-reader container="imported_pending_xacts.data">
-                    </span>
-                  </div>
-                <button class="btn btn-warning pull-right" ng-click="clear_pending()">[% l('Clear Transactions') %]</button>
-                </div>
-              </div>
-              <div class="row">
-                <div class="col-md-12 container">
-                  <table class="table">
-                    <thead>
-                      <tr>
-                        <th>[% l('Type') %]</th>
-                        <th>[% l('Timestamp') %]</th>
-                        <th>[% l('Patron Barcode') %]</th>
-                        <th>[% l('Item Barcode') %]</th>
-                        <th>[% l('Non-cataloged Type') %]</th>
-                        <th>[% l('Checkout Date') %]</th>
-                        <th>[% l('Due Date') %]</th>
-                        <th>[% l('Checkin Date') %]</th>
-                        <th>[% l('First Name') %]</th>
-                        <th>[% l('Last Name') %]</th>
-                      </tr>
-                    </thead>
-                    <tbody>
-                      <tr ng-repeat="xact in pending_xacts track by $index">
-                        <td>{{xact.type}}</td>
-                        <td>{{createDate(xact.timestamp, true) | date:'short'}}</td>
-                        <td>{{xact.patron_barcode || xact.user.card.barcode}}</td>
-                        <td>{{xact.barcode}}</td>
-                        <td>{{lookupNoncatTypeName(xact.noncat_type)}}</td>
-                        <td>{{createDate(xact.checkout_time) | date:'short'}}</td>
-                        <td>{{createDate(xact.due_date) | date:'shortDate'}}</td>
-                        <td>{{createDate(xact.backdate) | date:'shortDate'}}</td>
-                        <td>{{xact.user.first_given_name}}</td>
-                        <td>{{xact.user.family_name}}</td>
-                      </tr>
-                    </tbody>
-                  </table>
-                </div>
-              </div>
-            </uib-tab>
-            <uib-tab index="'offline_sessions'" heading="[% l('Offline Sessions') %]">
-              <div class="row">
-                <div class="col-md-12">
-                  <button
-                    class="btn btn-primary"
-                    ng-disabled="!logged_in"
-                    ng-click="createSession()">[% l('Create Session') %]</button>
-                  <button
-                    class="btn btn-default pull-right"
-                    ng-disabled="!logged_in"
-                    ng-click="refreshSessions()">[% l('Refresh') %]</button>
-                </div>
-              </div>
-              <div class="row">
-                <div class="col-md-12"><h2>[% l('Session List') %]</h2></div>
-              </div>
-              <div class="row">
-                <div class="col-md-12">
-                  <table class="table" ts-wrapper>
-                    <thead>
-                      <tr>
-                        <th ts-criteria="org">[% l('Organization') %]</th>
-                        <th ts-criteria="creator">[% l('Created By') %]</th>
-                        <th ts-criteria="description">[% l('Description') %]</th>
-                        <th ts-criteria="create_time|parseInt" ts-default="descending">[% l('Date Created') %]</th>
-                        <th>[% l('Upload Count') %]</th>
-                        <th>[% l('Transactions Processed') %]</th>
-                        <th ts-criteria="end_time|parseInt">[% l('Date Completed') %]</th>
-                        <th></th>
-                      </tr>
-                    </thead>
-                    <tbody>
-                      <tr ts-repeat
-                        ng-repeat="ses in sessions track by $index"
-                        ng-click="setSession(ses, $index)"
-                        ng-class="{'bg-info':current_session_index==$index}"
-                      >
-                        <td>{{ses.org}}</td>
-                        <td>{{ses.creator}}</td>
-                        <td>{{ses.description}}</td>
-                        <td>{{createDate(ses.create_time, true) | date:'short'}}</td>
-                        <td>{{ses.total}}</td>
-                        <td>{{ses.num_complete}}</td>
-                        <td>{{createDate(ses.end_time, true) | date:'short'}}</td>
-                        <td>
-                          <button
-                            class="btn btn-info btn-xs"
-                            ng-disabled="!logged_in || pending_xacts.length == 0 || ses.end_time"
-                            ng-click="uploadPending(ses, $index)"
-                          >[% l('Upload') %]</button>
-                          <button
-                            class="btn btn-warning btn-xs"
-                            ng-disabled="!logged_in || ses.total == 0 || ses.end_time"
-                            ng-click="processSession(ses, $index)"
-                          >[% l('Process') %]</button>
-                        </td>
-                      </tr>
-                    </tbody>
-                  </table>
-                </div>
-              </div>
-              <div class="row">
-                    <div class="col-md-12"><hr/></div>
-              </div>
-              <div class="row">
-                    <div class="col-md-12"><h2>[% l('Exception List') %]</h2></div>
-              </div>
-              <div class="row">
-                <div class="col-md-12">
-                  <table class="table">
-                    <thead>
-                      <tr>
-                        <th>[% l('Workstation') %]</th>
-                        <th>[% l('Type') %]</th>
-                        <th>[% l('Timestamp') %]</th>
-                        <th>[% l('Event Name') %]</th>
-                        <th>[% l('Patron Barcode') %]</th>
-                        <th>[% l('Item Barcode') %]</th>
-                        <th>[% l('Non-cataloged Type') %]</th>
-                        <th>[% l('Checkout Date') %]</th>
-                        <th>[% l('Due Date') %]</th>
-                        <th>[% l('Checkin Date') %]</th>
-                        <th></th>
-                      </tr>
-                    </thead>
-                    <tbody>
-                      <tr ng-repeat="xact in current_session.exceptions track by $index">
-                        <td>{{xact.command._workstation}}</td>
-                        <td>{{xact.command.type}}</td>
-                        <td>{{createDate(xact.command.timestamp, true) | date:'short'}}</td>
-                        <td>{{xact.event.textcode}}</td>
-                        <td>{{xact.command.patron_barcode || xact.command.user.card.barcode}}</td>
-                        <td>{{xact.command.barcode}}</td>
-                        <td>{{lookupNoncatTypeName(xact.command.noncat_type)}}</td>
-                        <td>{{createDate(xact.command.checkout_time) | date:'short'}}</td>
-                        <td>{{createDate(xact.command.due_date) | date:'shortDate'}}</td>
-                        <td>{{createDate(xact.command.backdate) | date:'shortDate'}}</td>
-                        <td>
-                          <button
-                            class="btn btn-info btn-xs"
-                            ng-disabled="!logged_in || !xact.command.barcode"
-                            ng-click="retrieveItem(xact.command.barcode)">[% l('Item') %]</button>
-                          <button
-                            class="btn btn-info btn-xs"
-                            ng-disabled="!logged_in || (!xact.command.patron_barcode && xact.command.user.card.barcode)"
-                            ng-click="retrievePatron(xact.command.patron_barcode)">[% l('Patron') %]</button>
-                          <button
-                            class="btn btn-info btn-xs"
-                            ng-disabled="!logged_in"
-                            ng-click="retrieveDetails(xact)">[% l('Debug') %]</button>
-                        </td>
-                      </tr>
-                    </tbody>
-                  </table>
-                </div>
-              </div>
-            </uib-tab>
-          </uib-tabset>
-        </div>
-      </uib-tab>
     </uib-tabset>
   </div>
 </div>
diff --git a/Open-ILS/web/js/ui/default/staff/offline.js b/Open-ILS/web/js/ui/default/staff/offline.js
index 12cfc3f..276333d 100644
--- a/Open-ILS/web/js/ui/default/staff/offline.js
+++ b/Open-ILS/web/js/ui/default/staff/offline.js
@@ -252,12 +252,12 @@ function($routeProvider , $locationProvider , $compileProvider) {
 .controller('OfflineCtrl', 
            ['$q','$scope','$window','$location','$rootScope','egCore','egLovefield','$routeParams','$timeout','$http','ngToast','egConfirmDialog','egUnloadPrompt',
     function($q , $scope , $window , $location , $rootScope , egCore , egLovefield , $routeParams , $timeout , $http , ngToast , egConfirmDialog , egUnloadPrompt) {
-        $scope.active_tab = $routeParams.tab || 'checkout';
 
         // Immediately redirect if we're really offline
         if (!$window.navigator.onLine) {
             if ($location.path().match(/session$/)) {
                 var path = $location.path();
+                console.log('internal redirect');
                 return $location.path(path.replace('session','checkout'));
             }
         }
@@ -325,8 +325,15 @@ function($routeProvider , $locationProvider , $compileProvider) {
 
         $scope.logged_in = egCore.auth.token() ? true : false;
 
-        if (!$scope.logged_in && $routeParams.tab == 'session')
-            $scope.active_tab = 'checkout';
+
+        $scope.active_tab = $routeParams.tab;
+        $timeout(function(){
+            if (!$scope.logged_in) {
+                $scope.active_tab = 'checkout';
+            } else {
+                $scope.active_tab = 'session';
+            }
+        });
         
         egCore.hatch.getItem('eg.offline.print_receipt')
         .then(function(setting) {
@@ -457,7 +464,7 @@ function($routeProvider , $locationProvider , $compileProvider) {
         }
 
         $rootScope.save_offline_xacts = function () { return $scope.save() };
-        $rootScope.active_tab = function (t) { $scope.active_tab = t };
+        //$rootScope.active_tab = function (t) { $scope.active_tab = t };
 
         $scope.logout = function () {
             egCore.auth.logout();
@@ -484,6 +491,7 @@ function($routeProvider , $locationProvider , $compileProvider) {
 
         $scope.retrieve_pending();
         $scope.$watch('active_tab', function (n,o) {
+            console.log('watch caught change to active_tab: ' + o + ' -> ' + n);
             if (n != o && !$scope.do_check_changed && n != 'checkout') $scope.strict_barcode = false;
             if (n != o && !$scope.do_check_changed && n == 'checkout') $scope.strict_barcode = true;
             if (n != o && !$scope.do_print_changed && n != 'checkout') $scope.do_print = false;

commit b77f4cb27f98bf0a673f0a7e9c286b2dd2362d4a
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Aug 29 14:14:34 2017 -0400

    Remove confusing "session" tab from the offline menu entry -- the code will figure out the correct default tab
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2
index 45e0068..1142bcc 100644
--- a/Open-ILS/src/templates/staff/navbar.tt2
+++ b/Open-ILS/src/templates/staff/navbar.tt2
@@ -225,7 +225,7 @@
           </li>
           <li class="divider"></li>
           <li>
-            <a href="./offline-interface/session" target="_self">
+            <a href="./offline-interface" target="_self">
               <span class="glyphicon glyphicon-alert"></span>
               <span>[% l('Offline Circulation') %]</span>
             </a>

commit 40079db66be4ab3450182362b1ff5ac684a449cf
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Aug 29 14:13:48 2017 -0400

    Add moment.js to the offline asset list
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/staff/base_js.tt2 b/Open-ILS/src/templates/staff/base_js.tt2
index 691b01c..7563869 100644
--- a/Open-ILS/src/templates/staff/base_js.tt2
+++ b/Open-ILS/src/templates/staff/base_js.tt2
@@ -18,6 +18,8 @@ UpUp.start({
     '[% ctx.base_path %]/staff/css/style.css',
     '[% ctx.base_path %]/staff/css/circ.css',
     '[% ctx.media_prefix %]/js/dojo/opensrf/md5.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/moment-with-locales.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/moment-timezone-with-data.min.js',
     '[% ctx.media_prefix %]/js/ui/default/staff/build/js/jquery.min.js',
     '[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular.min.js',
     '[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-route.min.js',

commit 2b843e55d59b31068197eb248294395af8b33602
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Aug 8 13:25:39 2017 -0400

    offline: Load Lovefield wrapper in addition to the Lovefield framework
    
    Spotted by Bill Erickson.  Thanks, Bill.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>
    
    Conflicts:
    	Open-ILS/web/js/ui/default/staff/test/karma.conf.js
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/test/karma.conf.js b/Open-ILS/web/js/ui/default/staff/test/karma.conf.js
index e704548..1150a07 100644
--- a/Open-ILS/web/js/ui/default/staff/test/karma.conf.js
+++ b/Open-ILS/web/js/ui/default/staff/test/karma.conf.js
@@ -46,6 +46,7 @@ module.exports = function(config){
       'services/grid.js',
       'services/op_change.js',
       'services/patron_search.js',
+      'services/lovefield.js',
       'services/navbar.js', 'services/date.js',
       'services/user-bucket.js',
       // load app scripts

commit 185fbc402af561165439ddf04e82cf0dab804da7
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Aug 7 11:12:53 2017 -0400

    offline: add live-test for offline assets
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/live_t/24-offline-all-assets.t b/Open-ILS/src/perlmods/live_t/24-offline-all-assets.t
new file mode 100644
index 0000000..334778f
--- /dev/null
+++ b/Open-ILS/src/perlmods/live_t/24-offline-all-assets.t
@@ -0,0 +1,7 @@
+#!perl
+
+use Test::More tests => 1;
+
+my $command = = 'wget --no-check-certificate -m https://localhost/eg/staff/offline-interface/session 2>&1 |grep -B 2 404|grep https|grep -v robots.txt|wc -l'
+is(`$command`, '0', "No missing assets required by the offline interface");
+

commit cb523260f300377b9977b98d1bb9010e0419581d
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Aug 7 10:05:05 2017 -0400

    offline: Remove reference to nonexistent file
    
    When the status bar was removed back in commit 7814064, the reference to its
    backing JS file was removed from base_js.tt2.  It looks like that slipped
    back in at some point in the combined serials/offline branch.  This removes
    that references, which causes offline failure.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/staff/base_js.tt2 b/Open-ILS/src/templates/staff/base_js.tt2
index 4093893..691b01c 100644
--- a/Open-ILS/src/templates/staff/base_js.tt2
+++ b/Open-ILS/src/templates/staff/base_js.tt2
@@ -55,7 +55,6 @@ UpUp.start({
     '[% ctx.media_prefix %]/js/ui/default/staff/services/coresvc.js',
     '[% ctx.media_prefix %]/js/ui/default/staff/services/user.js',
     '[% ctx.media_prefix %]/js/ui/default/staff/services/navbar.js',
-    '[% ctx.media_prefix %]/js/ui/default/staff/services/statusbar.js',
     '[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js',
     '[% ctx.media_prefix %]/js/ui/default/staff/services/date.js',
     '[% ctx.media_prefix %]/js/ui/default/staff/services/op_change.js',

commit 5305b78a3c63b85a770f873139aec4701077ae38
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Aug 4 11:16:17 2017 -0400

    offline: Load lovefield in the testing framework
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/test/karma.conf.js b/Open-ILS/web/js/ui/default/staff/test/karma.conf.js
index a5e9d26..e704548 100644
--- a/Open-ILS/web/js/ui/default/staff/test/karma.conf.js
+++ b/Open-ILS/web/js/ui/default/staff/test/karma.conf.js
@@ -6,6 +6,7 @@ module.exports = function(config){
     logLevel: config.LOG_INFO,
 
     files : [
+      'build/js/lovefield.min.js',
       'build/js/angular.min.js',
       'build/js/angular-route.min.js',
       'node_modules/angular-mocks/angular-mocks.js', // testing only

commit 09854bc35b46dc887e49ee307d71fa6ceab9813b
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Jul 27 12:59:43 2017 -0400

    offline: Prefer user-supplied param to browser-supplied cookie in the authen proxy
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/Proxy/Authen.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/Proxy/Authen.pm
index 5b1c64b..2366cfe 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/Proxy/Authen.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/Proxy/Authen.pm
@@ -39,8 +39,8 @@ sub handler {
     return Apache2::Const::NOT_FOUND unless (@$perms);
 
     my $cgi = new CGI;
-    my $auth_ses = $cgi->cookie('ses') || $cgi->param('ses');
-    my $ws_ou = $apache->dir_config('OILSProxyLoginOU') || $cgi->cookie('ws_ou') || $cgi->param('ws_ou');
+    my $auth_ses = $cgi->param('ses') || $cgi->cookie('ses');
+    my $ws_ou = $apache->dir_config('OILSProxyLoginOU') || $cgi->param('ws_ou') || $cgi->cookie('ws_ou');
 
     my $url = $cgi->url;
     my $bad_auth = 1; # Assume failure until proven otherwise ;)

commit 83dbb0001134b4323acb7e50b79c67d779aff1f5
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Jul 25 17:10:48 2017 -0400

    offline: Make sure the the field_doc structure exists before writing to it
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/offline.js b/Open-ILS/web/js/ui/default/staff/offline.js
index dab4065..12cfc3f 100644
--- a/Open-ILS/web/js/ui/default/staff/offline.js
+++ b/Open-ILS/web/js/ui/default/staff/offline.js
@@ -905,6 +905,8 @@ function($routeProvider , $locationProvider , $compileProvider) {
     service.get_field_doc = function() {
         return egLovefield.getListFromOfflineCache('fdoc').then(function (list) {
             angular.forEach(list, function(doc) {
+                if (!service.field_doc[doc.fm_class()])
+                    service.field_doc[doc.fm_class()] = {};
                 service.field_doc[doc.fm_class()][doc.field()] = doc;
             });
             return $q.when();

commit d94719306d63457ed25c03deb2d4ce07d01e8315
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed May 31 11:29:36 2017 -0400

    webstaff: IDL Clone
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/idl.js b/Open-ILS/web/js/ui/default/staff/services/idl.js
index 1b47ec1..29b7989 100644
--- a/Open-ILS/web/js/ui/default/staff/services/idl.js
+++ b/Open-ILS/web/js/ui/default/staff/services/idl.js
@@ -21,61 +21,31 @@ angular.module('egCoreMod')
     var service = {};
 
     // Clones data structures containing fieldmapper objects
-    service.Clone = function(old) {
+    service.Clone = function(old, depth) {
+        if (depth === undefined) depth = 100;
         var obj;
-        if (typeof old == 'undefined') {
+        if (typeof old == 'undefined' || old === null) {
             return old;
         } else if (old._isfieldmapper) {
             obj = new service[old.classname]()
-
-            for( var i in old.a ) {
-                var thing = old.a[i];
-                if(thing === null) continue;
-
-                if (typeof thing == 'undefined') {
-                    obj.a[i] = thing;
-                } else if (thing._isfieldmapper) {
-                    obj.a[i] = service.Clone(thing);
-                } else {
-
-                    if(angular.isArray(thing)) {
-                        obj.a[i] = [];
-
-                        for( var j in thing ) {
-
-                            if (typeof thing[j] == 'undefined')
-                                obj.a[i][j] = thing[j];
-                            else if( thing[j]._isfieldmapper )
-                                obj.a[i][j] = service.Clone(thing[j]);
-                            else
-                                obj.a[i][j] = angular.copy(thing[j]);
-                        }
-                    }
-                }
-            }
+            if (old.a) obj.a = service.Clone(old.a, depth); // pass same depth because we're still cloning this same object
         } else {
             if(angular.isArray(old)) {
                 obj = [];
-                for( var j in old ) {
-                    if (typeof old[j] == 'undefined')
-                        obj[j] = old[j];
-                    else if( old[j]._isfieldmapper )
-                        obj[j] = service.Clone(old[j]);
-                    else
-                        obj[j] = angular.copy(old[j]);
-                }
             } else if(angular.isObject(old)) {
                 obj = {};
-                for( var j in old ) {
-                    if (typeof old[j] == 'undefined')
-                        obj[j] = old[j];
-                    else if( old[j]._isfieldmapper )
-                        obj[j] = service.Clone(old[j]);
-                    else
-                        obj[j] = angular.copy(old[j]);
-                }
             } else {
-                obj = angular.copy(old);
+                 return angular.copy(old);
+            }
+
+            for( var j in old ) {
+                if (old[j] === null || typeof old[j] == 'undefined') {
+                    obj[j] = old[j];
+                } else if( old[j]._isfieldmapper ) {
+                    if (depth) obj[j] = service.Clone(old[j], depth - 1);
+                } else {
+                    obj[j] = angular.copy(old[j]);
+                }
             }
         }
         return obj;

commit 7c3cdbbd140865e07d08952422c82605ac8c5676
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Mar 20 16:38:15 2017 -0400

    LP#1706107: Offline mode
    
    Here is implemented an offline mode interface for the web staff client.
    
    It is made available during both network and server outages by using the
    UpUp[1] service worker wrapper.
    
    We leverage Lovefield[2] for local storage of library settings, configuration
    data, offline transactions, and the standalone offline block list.
    
    In order to make use of the offline interface, users should first log into
    the web staff client and navigate to the "Search -> Search for Patrons"
    interface, perform a search, select a user from the results, and open the
    Patron Editor interface.  This will allow the offline interface to collect
    all the relevant configuration information for the workstation.  In addition,
    the offline interface available from the Circulation menu provides a "Download
    block list" button when accessed while logged in.
    
    [1]https://www.talater.com/upup/
    [2]https://google.github.io/lovefield/
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>
    
    Conflicts:
    	Open-ILS/src/templates/staff/base_js.tt2
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/offline/offline.pl b/Open-ILS/src/offline/offline.pl
index f7719d5..4bc9e51 100755
--- a/Open-ILS/src/offline/offline.pl
+++ b/Open-ILS/src/offline/offline.pl
@@ -40,6 +40,7 @@ do '##CONFIG##/offline-config.pl';
 my $cgi			= new CGI;
 my $basedir		= $config{base_dir} || die "Offline config error: no base_dir defined\n";
 my $bootstrap	= $config{bootstrap} || die "Offline config error: no bootstrap defined\n";
+my $webclient	= $cgi->param('wc');
 my $wsname		= $cgi->param('ws');
 my $org			= $cgi->param('org');
 my $authtoken	= $cgi->param('ses') || "";
@@ -877,6 +878,7 @@ sub ol_handle_register {
 
 	my $barcode = $command->{user}->{card}->{barcode};
 	delete $command->{user}->{card}; 
+	delete $command->{user}->{cards} if $command->{user}->{cards}; 
 
 	$logger->info("offline: creating new user with barcode $barcode");
 
@@ -914,8 +916,10 @@ sub ol_handle_register {
 	delete $command->{user}->{survey_responses};
 	$actor->survey_responses(\@sresp) if @sresp;
 
+    my $bid = undef;
 	# extract the billing address
 	if( my $addr = $command->{user}->{billing_address} ) {
+        $bid = $command->{user}->{billing_address}->{id};
 		$billing_address = Fieldmapper::actor::user_address->new;
 		$billing_address->$_($addr->{$_}) for keys %$addr;
 		$billing_address->isnew(1);
@@ -925,15 +929,26 @@ sub ol_handle_register {
 		$logger->debug("offline: read billing address ".$billing_address->street1);
 	}
 
+    my $mid = undef;
 	# extract the mailing address
 	if( my $addr = $command->{user}->{mailing_address} ) {
-		$mailing_address = Fieldmapper::actor::user_address->new;
-		$mailing_address->$_($addr->{$_}) for keys %$addr;
-		$mailing_address->isnew(1);
-		$mailing_address->id(-2);
-		$mailing_address->usr(-1);
+        $mid = $command->{user}->{mailing_address}->{id};
+        if ($webclient && $mid != $bid) {
+		    $mailing_address = Fieldmapper::actor::user_address->new;
+		    $mailing_address->$_($addr->{$_}) for keys %$addr;
+		    $mailing_address->isnew(1);
+		    $mailing_address->id(-2);
+		    $mailing_address->usr(-1);
+		    $logger->debug("offline: read mailing address ".$mailing_address->street1);
+        } elsif (!$webclient) {
+		    $mailing_address = Fieldmapper::actor::user_address->new;
+		    $mailing_address->$_($addr->{$_}) for keys %$addr;
+		    $mailing_address->isnew(1);
+		    $mailing_address->id(-2);
+		    $mailing_address->usr(-1);
+		    $logger->debug("offline: read mailing address ".$mailing_address->street1);
+        }
 		delete $command->{user}->{mailing_address};
-		$logger->debug("offline: read mailing address ".$mailing_address->street1);
 	}
 
 	# make sure we have values for both
@@ -946,9 +961,23 @@ sub ol_handle_register {
 
 	push( @{$actor->addresses}, $billing_address ) 
 		unless $billing_address->id eq $mailing_address->id;
+
+    my $aid = -3;
+    for my $a ( @{$command->{user}->{addresses}} ) {
+        next if ($a->{id} == $bid || $a->{id} == $mid);
+    	# extract all other addresses
+        my $addr = Fieldmapper::actor::user_address->new;
+	    $addr->$_($a->{$_}) for keys %$a;
+		$addr->isnew(1);
+    	$addr->id($aid);
+	    $addr->usr(-1);
+    	$logger->debug("offline: read other address ".$addr->street1);
+        $aid--;
+        push( @{$actor->addresses}, $addr );
+    }
 	
 	# pull all of the rest of the data from the command blob
-	$actor->$_( $command->{user}->{$_} ) for keys %{$command->{user}};
+	$actor->$_( $command->{user}->{$_} ) for grep { $_ ne 'addresses' } keys %{$command->{user}};
 
     # calculate the expire date for the patron based on the profile group
     my ($grp) = grep {$_->id == $actor->profile} @$user_groups;
diff --git a/Open-ILS/src/templates/staff/admin/workstation/t_print_templates.tt2 b/Open-ILS/src/templates/staff/admin/workstation/t_print_templates.tt2
index 064bfff..12eccfb 100644
--- a/Open-ILS/src/templates/staff/admin/workstation/t_print_templates.tt2
+++ b/Open-ILS/src/templates/staff/admin/workstation/t_print_templates.tt2
@@ -33,6 +33,10 @@
           <option value="renew">[% l('Renew') %]</option>
           <option value="transit_list">[% l('Transit List') %]</option>
           <option value="transit_slip">[% l('Transit Slip') %]</option>
+          <option value="offline_checkout">[% l('Offline Checkout') %]</option>
+          <option value="offline_renew">[% l('Offline Renew') %]</option>
+          <option value="offline_checkin">[% l('Offline Checkin') %]</option>
+          <option value="offline_in_house_use">[% l('Offline In-house Use') %]</option>
         </select>
         <label for="print_context">[% l('Force Printer Context') %]</label>
         <select class="form-control" ng-model="print.template_context">
diff --git a/Open-ILS/src/templates/staff/base_js.tt2 b/Open-ILS/src/templates/staff/base_js.tt2
index 211eace..4093893 100644
--- a/Open-ILS/src/templates/staff/base_js.tt2
+++ b/Open-ILS/src/templates/staff/base_js.tt2
@@ -1,3 +1,80 @@
+<script src="/upup.min.js"></script>
+<script>
+UpUp.start({
+  'content-url': '[% ctx.base_path %]/staff/offline-interface',
+  'cache-version': '[% USE date(format = '%Y-%m-%d'); date.format; %]',
+  'service-worker-url': '/upup.sw.min.js',
+  'assets': [
+    '/IDL2js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/css/bootstrap.min.css',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/css/hotkeys.min.css',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/css/ngToast.min.css',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/css/ngToast-animations.min.css',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/css/tree-control.css',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/css/tree-control-attribute.css',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/css/tablesort.css',
+    '[% ctx.base_path %]/staff/css/print.css',
+    '[% ctx.base_path %]/staff/css/cat.css',
+    '[% ctx.base_path %]/staff/css/style.css',
+    '[% ctx.base_path %]/staff/css/circ.css',
+    '[% ctx.media_prefix %]/js/dojo/opensrf/md5.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/jquery.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-route.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/ui-bootstrap-tpls.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/hotkeys.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-file-saver.bundle.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-location-update.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-animate.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-sanitize.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-cookies.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/ngToast.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-tree-control.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/iframeResizer.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/ng-order-object-by.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-tablesort.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/js/lovefield.min.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/fonts/glyphicons-halflings-regular.woff',
+    '[% ctx.media_prefix %]/js/ui/default/staff/build/fonts/glyphicons-halflings-regular.woff2',
+    '[% ctx.media_prefix %]/js/dojo/opensrf/JSON_v1.js',
+    '[% ctx.media_prefix %]/js/dojo/opensrf/opensrf.js',
+    '[% ctx.media_prefix %]/js/dojo/opensrf/opensrf_ws.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/core.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/strings.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/idl.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/event.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/net.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/auth.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/pcrud.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/env.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/org.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/startup.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/hatch.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/print.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/audio.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/coresvc.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/user.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/navbar.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/statusbar.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/date.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/op_change.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/lovefield.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/services/file.js',
+    '[% ctx.media_prefix %]/js/ui/default/staff/offline.js',
+    '[% ctx.base_path %]/staff/share/t_alert_dialog',
+    '[% ctx.base_path %]/staff/share/t_confirm_dialog',
+    '[% ctx.base_path %]/staff/share/t_datetime',
+    '[% ctx.base_path %]/staff/share/t_progress_dialog',
+    '[% ctx.base_path %]/staff/share/print_templates/t_offline_in_house_use',
+    '[% ctx.base_path %]/staff/share/print_templates/t_offline_checkout',
+    '[% ctx.base_path %]/staff/share/print_templates/t_offline_checkin',
+    '[% ctx.base_path %]/staff/share/print_templates/t_offline_renew',
+    '/images/question-mark.png'
+  ]
+});
+</script>
+
 <script src="/IDL2js"></script>
 <script src="[% ctx.media_prefix %]/js/dojo/opensrf/md5.js"></script>
 
@@ -47,6 +124,7 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/date.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/op_change.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/lovefield.js"></script>
 
 [% ELSE %]
 
@@ -92,6 +170,19 @@
     s.OPT_IN_DIALOG_TITLE = "[% l('Verify Permission to Share Personal Information') %]";
     s.OPT_IN_DIALOG = "[% l('Does patron [_1], [_2] from [_3] ([_4]) consent to having their personal information shared with your library?', '{{family_name}}', '{{first_given_name}}', '{{org_name}}', '{{org_shortname}}') %]";
     s.OPT_IN_RESTRICTED = "[% l("This patron's record is not viewable at your library.") %]";
+    s.OFFLINE_SESSION_DESC = "[% l('Offline session description') %]";
+    s.OFFLINE_SESSION_CREATE_FAILED = "[% l('Offline session creation failed') %]";
+    s.OFFLINE_SESSION_PROCESSING_FAILED = "[% l('Offline session processing failed') %]";
+    s.OFFLINE_SESSION_UPLOAD_FAILED = "[% l('Offline transaction upload failed') %]";
+    s.PATRON_NOT_FOUND = "[% l('Patron not found') %]";
+    s.PATRON_BLOCKED = "[% l('Patron blocked') %]";
+    s.BAD_BARCODE = "[% l('Bad item barcode') %]";
+    s.BAD_BARCODE_CD = "[% l('Item barcode does not have a correct check digit.') %]";
+    s.BAD_PATRON_BARCODE = "[% l('Bad patron barcode') %]";
+    s.BAD_PATRON_BARCODE_CD = "[% l('Patron barcode does not have a correct check digit.') %]";
+    s.ITEM_NOT_FOUND = "[% l('Item not found') %]";
+    s.CONFIRM_CLEAR_PENDING = "[% l('Clear pending transactions') %]";
+    s.CONFIRM_CLEAR_PENDING_BODY = "[% l('Are you certain you want to clear these pending offline transactions? This action is irreversible. Transactions cannot be recovered after clearing!') %]";
   }]);
 </script>
 
diff --git a/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2 b/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
index 1508018..c5aabcb 100644
--- a/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
@@ -27,7 +27,7 @@
       ng-disabled="edit_passthru.hide_save_actions()"
       ng-click="edit_passthru.save()">[% l('Save') %]</button>
   </span>
-  <span class="pad-all-min">
+  <span ng-if="!offline" class="pad-all-min">
     <button type="button" class="btn btn-default"
       ng-disabled="edit_passthru.hide_save_actions()"
       ng_click="edit_passthru.save({clone:true})">[% l('Save & Clone') %]</button>
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 76649b7..3aeb43a 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -1,7 +1,7 @@
 [% DOC_IMG = '/images/question-mark.png' %]
 
 <!-- register banner -->
-<div ng-if="!patron_id" class='patron-reg-fixed-bar'>
+<div ng-if="!patron_id" ng-class='{"patron-reg-fixed-bar":!offline}'>
 
   <div class="container-fluid" style="text-align:center">
     <div class="alert alert-info alert-less-pad strong-text-2">
@@ -161,7 +161,7 @@ within the "form" by name for validation.
   <div class="col-md-6 patron-reg-example">
       <button class="btn btn-default" ng-show="!patron.isnew"
         ng-click="replace_card()">[% l('Replace Barcode') %]</button>
-      <button class="btn btn-default" 
+      <button class="btn btn-default" ng-if="!patron.isnew" 
         ng-click="cards_dialog()">[% l('See All') %]</button>
       <div ng-show="dupe_barcode" class="patron-reg-validation-alert">
         <span>[% l('Barcode is already in use') %]</span>
@@ -443,7 +443,7 @@ within the "form" by name for validation.
     </div>
   </div>
   <div class="col-md-3">
-    <button class="btn btn-default" ng-disabled="!perms.CREATE_USER_GROUP_LINK"
+    <button class="btn btn-default" ng-if="!offline" ng-disabled="!perms.CREATE_USER_GROUP_LINK"
       ng-click="secondary_groups_dialog()">[% l('Secondary Groups') %]</button>
   </div> 
 </div>
@@ -559,6 +559,8 @@ within the "form" by name for validation.
   </div>
 </div>
 
+<div ng-if="!offline">
+
 <div class="alert alert-success row" role="alert">
   <div class="col-md-6">[% l('User Settings') %]</div>
 </div>
@@ -677,6 +679,8 @@ within the "form" by name for validation.
   </div>
 </div>
 
+</div> <!-- end offline test -->
+
 <!-- addresses -->
 
 <div ng-repeat="addr in patron.addresses">
@@ -843,7 +847,8 @@ within the "form" by name for validation.
     class="btn btn-success">[% l('New Address') %]</button>
 </div>
 
-<div class="alert alert-success row" role="alert" 
+<div ng-if="!offline">
+<div class="alert alert-success row" role="alert"
     ng-show="show_field('stat_cats') || hasRequiredStatCat" ng-if="stat_cats.length > 0">
     <div class="col-md-6">[% l('Statistical Categories') %]</div>
 </div>
@@ -889,6 +894,7 @@ within the "form" by name for validation.
 
   </div><!-- show/hide wrapper -->
 </div>
+</div>
 
 <!-- surveys -->
 
diff --git a/Open-ILS/src/templates/staff/config.tt2 b/Open-ILS/src/templates/staff/config.tt2
index c563d7a..5d8b172 100644
--- a/Open-ILS/src/templates/staff/config.tt2
+++ b/Open-ILS/src/templates/staff/config.tt2
@@ -7,7 +7,7 @@ EVERGREEN_VERSION='0.0.1'
 # compressed build files.  Use this for development and debugging.
 EXPAND_WEB_IMPORTS = 1; 
 
-# path to build files (js, css, fonts)
-WEB_BUILD_PATH = ctx.media_prefix _ '/js/ui/default/staff/build/';
+# path to build files (js, css, fonts). No / at end, because the user supplies it
+WEB_BUILD_PATH = ctx.media_prefix _ '/js/ui/default/staff/build';
 
 %]
diff --git a/Open-ILS/src/templates/staff/index.tt2 b/Open-ILS/src/templates/staff/index.tt2
index 803774f..cd65d7a 100644
--- a/Open-ILS/src/templates/staff/index.tt2
+++ b/Open-ILS/src/templates/staff/index.tt2
@@ -6,6 +6,7 @@
 
 [% BLOCK APP_JS %]
 <!-- splash / login page app -->
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/lovefield.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/app.js"></script>
 [% END %]
 
diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2
index 912c43c..45e0068 100644
--- a/Open-ILS/src/templates/staff/navbar.tt2
+++ b/Open-ILS/src/templates/staff/navbar.tt2
@@ -68,7 +68,7 @@
         </a>
 
         <ul uib-dropdown-menu>
-          <li>
+          <li ng-if="username">
             <a href="./circ/patron/bcsearch" target="_self"
               eg-accesskey="[% l('f1') %]" 
               eg-accesskey-desc="[% l('Check Out') %]">
@@ -76,7 +76,15 @@
               [% l('Check Out') %]
             </a>
           </li>
-          <li>
+          <li ng-if="!username">
+            <a href="" ng-click="rs.active_tab('checkout')" target="_self"
+              eg-accesskey="[% l('f1') %]" 
+              eg-accesskey-desc="[% l('Check Out') %]">
+              <span class="glyphicon glyphicon-export"></span>
+              [% l('Check Out') %]
+            </a>
+          </li>
+          <li ng-if="username">
             <a href="./circ/checkin/checkin" target="_self"
               eg-accesskey="[% l('f2') %]" 
               eg-accesskey-desc="[% l('Check In') %]">
@@ -84,6 +92,14 @@
               [% l('Check In') %]
             </a>
           </li>
+          <li ng-if="!username">
+            <a href="" ng-click="rs.active_tab('checkin')" target="_self"
+              eg-accesskey="[% l('f2') %]" 
+              eg-accesskey-desc="[% l('Check In') %]">
+              <span class="glyphicon glyphicon-import"></span>
+              [% l('Check In') %]
+            </a>
+          </li>
           <li>
             <a href="./circ/checkin/capture" target="_self"
               eg-accesskey="[% l('shift+f2') %]" 
@@ -98,7 +114,7 @@
               [% l('Pull List for Hold Requests') %]
             </a>
           </li>
-          <li>
+          <li ng-if="username">
             <a href="./circ/renew/renew" target="_self"
               eg-accesskey="[% l('ctrl+f2') %]" 
               eg-accesskey-desc="[% l('Renew items') %]">
@@ -106,7 +122,15 @@
               [% l('Renew Items') %]
             </a>
           </li>
-          <li>
+          <li ng-if="!username">
+            <a href="" ng-click="rs.active_tab('renew')" target="_self"
+              eg-accesskey="[% l('ctrl+f2') %]" 
+              eg-accesskey-desc="[% l('Renew items') %]">
+              <span class="glyphicon glyphicon-refresh"></span>
+              [% l('Renew Items') %]
+            </a>
+          </li>
+          <li ng-if="username">
             <a href="./circ/patron/register" target="_self"
               eg-accesskey="[% l('shift+f1') %]" 
               eg-accesskey-desc="[% l('Register Patron') %]">
@@ -114,6 +138,14 @@
               [% l('Register Patron') %]
             </a>
           </li>
+          <li ng-if="!username">
+            <a href="" ng-click="rs.active_tab('register')" target="_self"
+              eg-accesskey="[% l('shift+f1') %]" 
+              eg-accesskey-desc="[% l('Register Patron') %]">
+              <span class="glyphicon glyphicon-user"></span>
+              [% l('Register Patron') %]
+            </a>
+          </li>
           <li>
             <a href="./circ/patron/last" target="_self"
               eg-accesskey="[% l('f8') %]" 
@@ -141,7 +173,7 @@
               <span>[% l('Verify Credentials') %]</span>
             </a>
           </li>
-          <li>
+          <li ng-if="username">
             <a href="./circ/in_house_use/index" target="_self"
               eg-accesskey="[% l('f6') %]" 
               eg-accesskey-desc="[% l('Record In-House Use') %]">
@@ -149,6 +181,14 @@
               <span>[% l('Record In-House Use') %]</span>
             </a>
           </li>
+          <li ng-if="!username">
+            <a href="" ng-click="rs.active_tab('in_house_use')" target="_self"
+              eg-accesskey="[% l('f6') %]" 
+              eg-accesskey-desc="[% l('Record In-House Use') %]">
+              <span class="glyphicon glyphicon-pencil"></span>
+              <span>[% l('Record In-House Use') %]</span>
+            </a>
+          </li>
           <li>
             <a href="./circ/holds/shelf" target="_self">
               <span class="glyphicon glyphicon-tasks"></span>
@@ -183,6 +223,13 @@
               <span>[% l('Reprint Last Receipt') %]</span>
             </a>
           </li>
+          <li class="divider"></li>
+          <li>
+            <a href="./offline-interface/session" target="_self">
+              <span class="glyphicon glyphicon-alert"></span>
+              <span>[% l('Offline Circulation') %]</span>
+            </a>
+          </li>
         </ul>
       </li><!-- circ -->
 
diff --git a/Open-ILS/src/templates/staff/offline-interface.tt2 b/Open-ILS/src/templates/staff/offline-interface.tt2
new file mode 100644
index 0000000..0cc81fd
--- /dev/null
+++ b/Open-ILS/src/templates/staff/offline-interface.tt2
@@ -0,0 +1,632 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Offline"); 
+  ctx.page_app = "egOffline";
+%]
+
+
+<script type="text/ng-template" id="offline-template">
+
+<div class="row">
+  <div class="col-md-12">
+    <div class="input-group">
+      <div class="input-group-addon"><b>[% l('Workstation') %]</b></div>
+      <div class="input-group-addon">
+        <select class="form-control" required
+          ng-model="workstation"
+          ng-options="ws.id as ws.name for ws in workstations"></select>
+      </div>
+      <div class="input-group-addon"><b>[% l('Working location') %]</b></div>
+      <div class="input-group-addon">
+        <eg-org-selector sticky-setting="eg.org.offline_location" selected="org"></eg-org-selector>
+      </div>
+      <div class="input-group-addon">
+        <button
+          class="btn btn-primary"
+          ng-disabled="all_xact.length == 0 || active_tab == 'register'"
+          ng-click="save()">
+            [% l('Save Transactions') %]
+        </button>
+        <button
+          class="btn btn-default"
+          ng-disabled="!printed"
+          ng-click="reprintLast()">
+            [% l('Reprint Last Receipt') %]
+        </button>
+        <button
+          class="btn btn-default"
+          ng-if="logged_in"
+          ng-click="downloadBlockList()">
+            [% l('Download block list') %]
+        </button>
+        <button
+          class="btn btn-default"
+          ng-disabled="pending_xacts.length == 0"
+          eg-line-exporter
+          ng-if="!logged_in"
+          default-file-name="pending.xacts"
+          json-array="pending_xacts"
+        >[% l('Export Transactions') %]</button>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="row col-md-offset-3 col-md-6 pad-vert">
+  <div ng-show="logged_in && active_tab != 'session'" class="alert alert-danger">
+    <h2>[% l('Warning') %]</h2>
+    [% l('You are about to enter offline mode. If you proceed, you will be logged out.') %]
+    <br/>
+    <br/>
+    <button class="btn btn-danger" ng-click="logout()">[% l('Proceed') %]</button>
+  </div>
+</div>
+
+<div class="row col-md-12 pad-vert">
+  <div class="col-md-12">
+    <uib-tabset active="active_tab">
+      <!-- note that non-numeric index values must be enclosed in single-quotes,
+           otherwise selecting the active table won't work cleanly -->
+      <uib-tab index="'checkout'" heading="[% l('Checkout') %]">
+
+        <div class="row">
+
+          <!-- left-hand side -->
+          <div class="col-md-6" style="border-right:solid 1px;">
+            <div class="row">
+              <div class="col-md-1"></div>
+              <div class="col-md-4">
+                [% l('Due Date:') %]
+              </div>
+              <div class="col-md-4">
+                <eg-date-input id="co_duedate" ng-model="shared.due_date" out-of-range="shared.outOfRange" min-date="minDate"></eg-date-input>
+              </div>
+              <div class="col-md-3">
+                <select class="form-control" ng-model="shared.due_date_offset" ng-change="resetDueDate()">
+                  <option value="">[% l('No Offset') %]</option>
+                  <option value="3">[% l('Today + 3 days') %]</option>
+                  <option value="7">[% l('Today + 7 days') %]</option>
+                  <option value="14">[% l('Today + 14 days') %]</option>
+                  <option value="30">[% l('Today + 30 days') %]</option>
+                </select>
+              </div>
+            </div>
+
+            <div class="row pad-vert">
+              <div class="col-md-1"></div>
+              <div class="col-md-4">
+                [% l('Patron barcode:') %]
+              </div>
+              <div class="col-md-7">
+                <input class="form-control" type="text" ng-model="checkout.patron_barcode" next-on-enter="co_barcode"/>
+              </div>
+            </div>
+
+            <div class="row pad-vert">
+              <div class="col-md-1">
+                <input type="radio" ng-model="barcode_type" value="barcode" id="bc_radio"/>
+              </div>
+              <div class="col-md-4">
+                <label style="font-weight:normal !important;" for="bc_radio">[% l('Item Barcode:') %]</label>
+              </div>
+              <div class="col-md-7">
+                <input id="co_barcode"
+                    class="form-control"
+                    ng-init="barcode_type = 'barcode'"
+                    ng-disabled="barcode_type != 'barcode'"
+                    type="text"
+                    ng-model="checkout.barcode"
+                    eg-enter="!notEnough('checkout') && add('checkout')"
+                />
+              </div>
+            </div>
+
+            <div class="row">
+              <div class="col-md-1">
+                <input type="radio" ng-model="barcode_type" value="noncat" id="nc_radio"/>
+              </div>
+              <div class="col-md-4">
+                <label style="font-weight:normal !important;" for="nc_radio">[% l('Non-cataloged Type:') %]</label>
+              </div>
+              <div class="col-md-5">
+                <select
+                    class="form-control"
+                    ng-disabled="barcode_type != 'noncat'"
+                    ng-options="nct.id() as nct.name() for nct in noncats"
+                    ng-model="checkout.noncat_type"
+                >
+                  <option value="">[% l('Select Non-cataloged Type') %]</option>
+                </select>
+              </div>
+              <div class="col-md-2">
+                <input
+                    class="form-control"
+                    ng-disabled="barcode_type != 'noncat'"
+                    type="number"
+                    min="1"
+                    max="100"
+                    ng-model="checkout.noncat_count"
+                />
+              </div>
+            </div>
+
+            <div class="row pad-vert">
+              <div class="col-md-2">
+                <button class="btn btn-warning" ng-click="clear('checkout')">[% l('Clear') %]</button>
+              </div>
+              <div class="col-md-4">
+                <input id="do_check_co" type="checkbox" ng-model="strict_barcode" ng-click="changeCheck()"></input>
+                <label for="do_check_co">[% l('Strict Barcode') %]</label>
+              </div>
+              <div class="col-md-6">
+                <input id="do_print_co" type="checkbox" ng-model="do_print" ng-click="changePrint()"></input>
+                <label for="do_print_co">[% l('Print receipt') %]</label>
+                <button class="btn btn-primary pull-right" ng-disabled="notEnough('checkout')" ng-click="add('checkout','co_barcode')">[% l('Checkout') %]</button>
+              </div>
+            </div>
+
+          </div>
+
+          <!-- right-hand side -->
+          <div class="col-md-6 container">
+            <table class="table">
+              <thead>
+                <tr>
+                  <th>[% l('Patron barcode') %]</th>
+                  <th>[% l('Item barcode') %]</th>
+                  <th>[% l('Due date') %]</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr ng-repeat="xact in xact_page.checkout track by $index">
+                  <td>{{xact.patron_barcode}}</td>
+                  <td>{{xact.barcode}}</td>
+                  <td>{{xact.due_date | date:'shortDate'}}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+
+        </div>
+
+      </uib-tab>
+      <uib-tab index="'renew'" heading="[% l('Renew') %]">
+
+        <div class="row">
+
+          <!-- left-hand side -->
+          <div class="col-md-6" style="border-right:solid 1px;">
+            <div class="row">
+              <div class="col-md-1"></div>
+              <div class="col-md-4">
+                [% l('Due Date:') %]
+              </div>
+              <div class="col-md-4">
+                <eg-date-input ng-model="shared.due_date" out-of-range="shared.outOfRange" min-date="minDate"></eg-date-input>
+              </div>
+              <div class="col-md-3">
+                <select class="form-control" ng-model="shared.due_date_offset" ng-change="resetDueDate()">
+                  <option value="">[% l('No Offset') %]</option>
+                  <option value="3">[% l('Today + 3 days') %]</option>
+                  <option value="7">[% l('Today + 7 days') %]</option>
+                  <option value="14">[% l('Today + 14 days') %]</option>
+                  <option value="30">[% l('Today + 30 days') %]</option>
+                </select>
+              </div>
+            </div>
+
+            <div class="row pad-vert">
+              <div class="col-md-1"></div>
+              <div class="col-md-4">
+                [% l('Patron barcode:') %]
+              </div>
+              <div class="col-md-7">
+                <input class="form-control" type="text" ng-model="renew.patron_barcode" next-on-enter="re_barcode"/>
+              </div>
+            </div>
+
+            <div class="row pad-vert">
+              <div class="col-md-1"></div>
+              <div class="col-md-4">
+                [% l('Item Barcode:') %]
+              </div>
+              <div class="col-md-7">
+                <input class="form-control" type="text" ng-model="renew.barcode" id="re_barcode" eg-enter="!notEnough('renew') && add('renew')"/>
+              </div>
+            </div>
+
+            <div class="row pad-vert">
+              <div class="col-md-2">
+                <button class="btn btn-warning" ng-click="clear('renew')">[% l('Clear') %]</button>
+              </div>
+              <div class="col-md-4">
+                <input id="do_check_r" type="checkbox" ng-model="strict_barcode" ng-click="changeCheck()"></input>
+                <label for="do_check_r">[% l('Strict Barcode') %]</label>
+              </div>
+              <div class="col-md-6">
+                <input id="do_print_r" type="checkbox" ng-model="do_print" ng-click="changePrint()"></input>
+                <label for="do_print_r">[% l('Print receipt') %]</label>
+                <button class="btn btn-primary pull-right" ng-disabled="notEnough('renew')" ng-click="add('renew','re_barcode')">[% l('Renew') %]</button>
+              </div>
+            </div>
+
+          </div>
+
+          <!-- right-hand side -->
+          <div class="col-md-6 container">
+            <table class="table">
+              <thead>
+                <tr>
+                  <th>[% l('Patron barcode') %]</th>
+                  <th>[% l('Item barcode') %]</th>
+                  <th>[% l('Due date') %]</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr ng-repeat="xact in xact_page.renew track by $index">
+                  <td>{{xact.patron_barcode}}</td>
+                  <td>{{xact.barcode}}</td>
+                  <td>{{xact.due_date | date:'shortDate'}}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+
+        </div>
+
+      </uib-tab>
+      <uib-tab index="'in_house_use'" heading="[% l('In-house Use') %]">
+
+        <div class="row">
+
+          <!-- left-hand side -->
+          <div class="col-md-6 container" style="border-right:solid 1px;">
+
+            <div class="row">
+              <div class="col-md-1"></div>
+              <div class="col-md-5">
+                [% l('Use count:') %]
+              </div>
+              <div class="col-md-6">
+                <input class="form-control" type="number" min="1" max="100" next-on-enter="ihu_barcode" ng-model="in_house_use.count"/>
+              </div>
+            </div>
+
+            <div class="row pad-vert">
+              <div class="col-md-1"></div>
+              <div class="col-md-5">
+                [% l('Item Barcode:') %]
+              </div>
+              <div class="col-md-6">
+                <input class="form-control" type="text" ng-model="in_house_use.barcode" eg-enter="add('in_house_use')" id="ihu_barcode"/>
+              </div>
+            </div>
+
+            <div class="row pad-vert">
+              <div class="col-md-2">
+                <button class="btn btn-warning" ng-click="clear('in_house_use')">[% l('Clear') %]</button>
+              </div>
+              <div class="col-md-4">
+                <input id="do_check_ihu" type="checkbox" ng-model="strict_barcode" ng-click="changeCheck()"></input>
+                <label for="do_check_ihu">[% l('Strict Barcode') %]</label>
+              </div>
+              <div class="col-md-6">
+                <input id="do_print_ihu" type="checkbox" ng-model="do_print" ng-click="changePrint()"></input>
+                <label for="do_print_ihu">[% l('Print receipt') %]</label>
+                <button class="btn btn-primary pull-right" ng-disabled="notEnough('in_house_use')" ng-click="add('in_house_use','ihu_barcode')">[% l('Record Use') %]</button>
+              </div>
+            </div>
+
+          </div>
+
+          <!-- right-hand side -->
+          <div class="col-md-6 container">
+            <table class="table">
+              <thead>
+                <tr>
+                  <th>[% l('Item barcode') %]</th>
+                  <th>[% l('Use count') %]</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr ng-repeat="xact in xact_page.in_house_use track by $index">
+                  <td>{{xact.barcode}}</td>
+                  <td>{{xact.count}}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+
+        </div>
+
+      </uib-tab>
+      <uib-tab index="'checkin'" heading="[% l('Checkin') %]">
+
+        <div class="row">
+
+          <!-- left-hand side -->
+          <div class="col-md-6" style="border-right:solid 1px;">
+
+            <div class="row">
+              <div class="col-md-1"></div>
+              <div class="col-md-5">
+                [% l('Checkin Date:') %]
+              </div>
+              <div class="col-md-6">
+                <eg-date-input ng-model="checkin.backdate"></eg-date-input>
+              </div>
+            </div>
+
+            <div class="row pad-vert">
+              <div class="col-md-1"></div>
+              <div class="col-md-5">
+                [% l('Item Barcode:') %]
+              </div>
+              <div class="col-md-6">
+                <input id="ci_barcode" class="form-control" type="text" ng-model="checkin.barcode" eg-enter="!notEnough('checkin') && add('checkin')"/>
+              </div>
+            </div>
+
+            <div class="row pad-vert">
+              <div class="col-md-2">
+                <button class="btn btn-warning" ng-click="clear('checkin')">[% l('Clear') %]</button>
+              </div>
+              <div class="col-md-4">
+                <input id="do_check_ci" type="checkbox" ng-model="strict_barcode" ng-click="changeCheck()"></input>
+                <label for="do_check_ci">[% l('Strict Barcode') %]</label>
+              </div>
+              <div class="col-md-6">
+                <input id="do_print_ci" type="checkbox" ng-model="do_print" ng-click="changePrint()"></input>
+                <label for="do_print_ci">[% l('Print receipt') %]</label>
+                <button class="btn btn-primary pull-right" ng-disabled="notEnough('checkin')" ng-click="add('checkin','ci_barcode')">[% l('Checkin') %]</button>
+                    
+              </div>
+            </div>
+
+          </div>
+
+          <!-- right-hand side -->
+          <div class="col-md-6 container">
+            <table class="table">
+              <thead>
+                <tr>
+                  <th>[% l('Item barcode') %]</th>
+                  <th>[% l('Effective Checkin date') %]</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr ng-repeat="xact in xact_page.checkin track by $index">
+                  <td>{{xact.barcode}}</td>
+                  <td>{{xact.backdate | date:'shortDate'}}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+
+        </div>
+
+      </uib-tab>
+      <uib-tab index="'register'" heading="[% l('Register Patron') %]">
+        <div ng-controller="PatronRegCtrl">
+          <div>[% INCLUDE 'staff/circ/patron/t_edit.tt2' %]</div>
+        </div>
+      </uib-tab>
+      <uib-tab ng-if="logged_in" index="'session'" heading="[% l('Session Management') %]">
+        <div class="col-md-12" ng-controller="OfflineSessionCtrl">
+          <uib-tabset active="active_session_tab">
+            <uib-tab index="'pending'" heading="[% l('Pending Transactions') %]">
+              <div class="row">
+                <div class="col-md-12 container">
+                  <button
+                    class="btn btn-default"
+                    ng-disabled="pending_xacts.length == 0"
+                    eg-line-exporter
+                    default-file-name="pending.xacts"
+                    json-array="pending_xacts"
+                  >[% l('Export Transactions') %]</button>
+                  <div class="btn-group">
+                    <span class="btn btn-default btn-file">
+                      [% l('Import Transactions') %]
+                      <input type="file" eg-file-reader container="imported_pending_xacts.data">
+                    </span>
+                  </div>
+                <button class="btn btn-warning pull-right" ng-click="clear_pending()">[% l('Clear Transactions') %]</button>
+                </div>
+              </div>
+              <div class="row">
+                <div class="col-md-12 container">
+                  <table class="table">
+                    <thead>
+                      <tr>
+                        <th>[% l('Type') %]</th>
+                        <th>[% l('Timestamp') %]</th>
+                        <th>[% l('Patron Barcode') %]</th>
+                        <th>[% l('Item Barcode') %]</th>
+                        <th>[% l('Non-cataloged Type') %]</th>
+                        <th>[% l('Checkout Date') %]</th>
+                        <th>[% l('Due Date') %]</th>
+                        <th>[% l('Checkin Date') %]</th>
+                        <th>[% l('First Name') %]</th>
+                        <th>[% l('Last Name') %]</th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      <tr ng-repeat="xact in pending_xacts track by $index">
+                        <td>{{xact.type}}</td>
+                        <td>{{createDate(xact.timestamp, true) | date:'short'}}</td>
+                        <td>{{xact.patron_barcode || xact.user.card.barcode}}</td>
+                        <td>{{xact.barcode}}</td>
+                        <td>{{lookupNoncatTypeName(xact.noncat_type)}}</td>
+                        <td>{{createDate(xact.checkout_time) | date:'short'}}</td>
+                        <td>{{createDate(xact.due_date) | date:'shortDate'}}</td>
+                        <td>{{createDate(xact.backdate) | date:'shortDate'}}</td>
+                        <td>{{xact.user.first_given_name}}</td>
+                        <td>{{xact.user.family_name}}</td>
+                      </tr>
+                    </tbody>
+                  </table>
+                </div>
+              </div>
+            </uib-tab>
+            <uib-tab index="'offline_sessions'" heading="[% l('Offline Sessions') %]">
+              <div class="row">
+                <div class="col-md-12">
+                  <button
+                    class="btn btn-primary"
+                    ng-disabled="!logged_in"
+                    ng-click="createSession()">[% l('Create Session') %]</button>
+                  <button
+                    class="btn btn-default pull-right"
+                    ng-disabled="!logged_in"
+                    ng-click="refreshSessions()">[% l('Refresh') %]</button>
+                </div>
+              </div>
+              <div class="row">
+                <div class="col-md-12"><h2>[% l('Session List') %]</h2></div>
+              </div>
+              <div class="row">
+                <div class="col-md-12">
+                  <table class="table" ts-wrapper>
+                    <thead>
+                      <tr>
+                        <th ts-criteria="org">[% l('Organization') %]</th>
+                        <th ts-criteria="creator">[% l('Created By') %]</th>
+                        <th ts-criteria="description">[% l('Description') %]</th>
+                        <th ts-criteria="create_time|parseInt" ts-default="descending">[% l('Date Created') %]</th>
+                        <th>[% l('Upload Count') %]</th>
+                        <th>[% l('Transactions Processed') %]</th>
+                        <th ts-criteria="end_time|parseInt">[% l('Date Completed') %]</th>
+                        <th></th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      <tr ts-repeat
+                        ng-repeat="ses in sessions track by $index"
+                        ng-click="setSession(ses, $index)"
+                        ng-class="{'bg-info':current_session_index==$index}"
+                      >
+                        <td>{{ses.org}}</td>
+                        <td>{{ses.creator}}</td>
+                        <td>{{ses.description}}</td>
+                        <td>{{createDate(ses.create_time, true) | date:'short'}}</td>
+                        <td>{{ses.total}}</td>
+                        <td>{{ses.num_complete}}</td>
+                        <td>{{createDate(ses.end_time, true) | date:'short'}}</td>
+                        <td>
+                          <button
+                            class="btn btn-info btn-xs"
+                            ng-disabled="!logged_in || pending_xacts.length == 0 || ses.end_time"
+                            ng-click="uploadPending(ses, $index)"
+                          >[% l('Upload') %]</button>
+                          <button
+                            class="btn btn-warning btn-xs"
+                            ng-disabled="!logged_in || ses.total == 0 || ses.end_time"
+                            ng-click="processSession(ses, $index)"
+                          >[% l('Process') %]</button>
+                        </td>
+                      </tr>
+                    </tbody>
+                  </table>
+                </div>
+              </div>
+              <div class="row">
+                    <div class="col-md-12"><hr/></div>
+              </div>
+              <div class="row">
+                    <div class="col-md-12"><h2>[% l('Exception List') %]</h2></div>
+              </div>
+              <div class="row">
+                <div class="col-md-12">
+                  <table class="table">
+                    <thead>
+                      <tr>
+                        <th>[% l('Workstation') %]</th>
+                        <th>[% l('Type') %]</th>
+                        <th>[% l('Timestamp') %]</th>
+                        <th>[% l('Event Name') %]</th>
+                        <th>[% l('Patron Barcode') %]</th>
+                        <th>[% l('Item Barcode') %]</th>
+                        <th>[% l('Non-cataloged Type') %]</th>
+                        <th>[% l('Checkout Date') %]</th>
+                        <th>[% l('Due Date') %]</th>
+                        <th>[% l('Checkin Date') %]</th>
+                        <th></th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      <tr ng-repeat="xact in current_session.exceptions track by $index">
+                        <td>{{xact.command._workstation}}</td>
+                        <td>{{xact.command.type}}</td>
+                        <td>{{createDate(xact.command.timestamp, true) | date:'short'}}</td>
+                        <td>{{xact.event.textcode}}</td>
+                        <td>{{xact.command.patron_barcode || xact.command.user.card.barcode}}</td>
+                        <td>{{xact.command.barcode}}</td>
+                        <td>{{lookupNoncatTypeName(xact.command.noncat_type)}}</td>
+                        <td>{{createDate(xact.command.checkout_time) | date:'short'}}</td>
+                        <td>{{createDate(xact.command.due_date) | date:'shortDate'}}</td>
+                        <td>{{createDate(xact.command.backdate) | date:'shortDate'}}</td>
+                        <td>
+                          <button
+                            class="btn btn-info btn-xs"
+                            ng-disabled="!logged_in || !xact.command.barcode"
+                            ng-click="retrieveItem(xact.command.barcode)">[% l('Item') %]</button>
+                          <button
+                            class="btn btn-info btn-xs"
+                            ng-disabled="!logged_in || (!xact.command.patron_barcode && xact.command.user.card.barcode)"
+                            ng-click="retrievePatron(xact.command.patron_barcode)">[% l('Patron') %]</button>
+                          <button
+                            class="btn btn-info btn-xs"
+                            ng-disabled="!logged_in"
+                            ng-click="retrieveDetails(xact)">[% l('Debug') %]</button>
+                        </td>
+                      </tr>
+                    </tbody>
+                  </table>
+                </div>
+              </div>
+            </uib-tab>
+          </uib-tabset>
+        </div>
+      </uib-tab>
+    </uib-tabset>
+  </div>
+</div>
+
+</script>
+
+[% BLOCK APP_JS %]
+<!-- offline page app -->
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/file.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/offline.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-tablesort.js"></script>
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+  s.OFFLINE_BLOCKLIST_SUCCESS = "[% l('Offline blocklist downloaded') %]";
+  s.OFFLINE_BLOCKLIST_FAIL = "[% l('Error downloading offline blocklist') %]";
+  s.DUPLICATE_BARCODE = "[% l('Duplicate item barcode') %]";
+
+  s.ALLOW = "[% l('Allow') %]";
+  s.REJECT = "[% l('Reject') %]";
+
+  s.REG_ADDR_TYPE = "[% l('Mailing') %]";
+  s.REG_INVALID_FIELDS =
+    "[% l('Please enter valid values for all required fields.') %]"
+  s.REG_ADDR_REQUIRED =
+    "[% l('An address is required during registration.') %]"
+
+  s.PATRON_BLOCKED_WHY = {};
+  s.PATRON_BLOCKED_WHY.D = "[% l('Patron has penalties') %]";
+  s.PATRON_BLOCKED_WHY.L = "[% l('Barcode is reported Lost') %]";
+  s.PATRON_BLOCKED_WHY.E = "[% l('Patron account is Expired') %]";
+  s.PATRON_BLOCKED_WHY.B = "[% l('Patron account is Barred') %]";
+
+}]);
+</script>
+<link rel="stylesheet" href="[% ctx.base_path %]/staff/css/circ.css" />
+<link rel="stylesheet" href="[% ctx.media_prefix %]/js/ui/default/staff/build/css/tablesort.css" />
+[% END %]
+
+<div ng-view></div> 
+
+[% END %]
+
diff --git a/Open-ILS/src/templates/staff/share/print_templates/t_offline_checkin.tt2 b/Open-ILS/src/templates/staff/share/print_templates/t_offline_checkin.tt2
new file mode 100644
index 0000000..7c0acdd
--- /dev/null
+++ b/Open-ILS/src/templates/staff/share/print_templates/t_offline_checkin.tt2
@@ -0,0 +1,25 @@
+<!--
+Template for printing offline checkout receipts; fields available include:
+
+* transactions - list of loans made during this session. Each
+  includes:
+
+  * barcode
+  * backdate
+  * timestamp
+
+-->
+<div>
+  <div>[% l('You checked in the following [_1] items:', '{{transactions.length}}') %]</div>
+  <hr/>
+  <ol>
+    <li ng-repeat="checkin in transactions">
+      <div>[% l('Barcode: [_1] Checkin date: [_2]', 
+        '{{checkin.barcode}}',
+        '{{checkin.backdate | date:"short"}}') %]</div>
+    </li>
+  </ol>
+  <hr/>
+  <div>{{today | date:'short'}}</div>
+<br/>
+
diff --git a/Open-ILS/src/templates/staff/share/print_templates/t_offline_checkout.tt2 b/Open-ILS/src/templates/staff/share/print_templates/t_offline_checkout.tt2
new file mode 100644
index 0000000..292e7b4
--- /dev/null
+++ b/Open-ILS/src/templates/staff/share/print_templates/t_offline_checkout.tt2
@@ -0,0 +1,26 @@
+<!--
+Template for printing offline checkout receipts; fields available include:
+
+* transactions - list of loans made during this session. Each
+  includes:
+
+  * barcode
+  * patron_barcode
+  * due_date
+
+-->
+<div>
+  <div>[% l('Patron [_1]', '{{transactions[0].patron_barcode}}') %]</div>
+  <div>[% l('You checked out the following [_1] items:', '{{transactions.length}}') %]</div>
+  <hr/>
+  <ol>
+    <li ng-repeat="checkout in transactions">
+      <div>[% l('Barcode: [_1] Due: [_2]', 
+        '{{checkout.barcode}}',
+        '{{checkout.due_date | date:"short"}}') %]</div>
+    </li>
+  </ol>
+  <hr/>
+  <div>{{today | date:'short'}}</div>
+<br/>
+
diff --git a/Open-ILS/src/templates/staff/share/print_templates/t_offline_in_house_use.tt2 b/Open-ILS/src/templates/staff/share/print_templates/t_offline_in_house_use.tt2
new file mode 100644
index 0000000..1f177ce
--- /dev/null
+++ b/Open-ILS/src/templates/staff/share/print_templates/t_offline_in_house_use.tt2
@@ -0,0 +1,24 @@
+<!--
+Template for printing offline in-house use receipts; fields available include:
+
+* transactions - list of loans made during this session. Each
+  includes:
+
+  * barcode
+  * count
+
+-->
+<div>
+  <div>[% l('You recorded use for the following [_1] items:', '{{transactions.length}}') %]</div>
+  <hr/>
+  <ol>
+    <li ng-repeat="checkout in transactions">
+      <div>[% l('Barcode [_1] used [_2] times', 
+        '{{checkout.barcode}}',
+        '{{checkout.count}}') %]</div>
+    </li>
+  </ol>
+  <hr/>
+  <div>{{today | date:'short'}}</div>
+<br/>
+
diff --git a/Open-ILS/src/templates/staff/share/print_templates/t_offline_renew.tt2 b/Open-ILS/src/templates/staff/share/print_templates/t_offline_renew.tt2
new file mode 100644
index 0000000..7bd8242
--- /dev/null
+++ b/Open-ILS/src/templates/staff/share/print_templates/t_offline_renew.tt2
@@ -0,0 +1,24 @@
+<!--
+Template for printing offline renew receipts; fields available include:
+
+* transactions - list of loans made during this session. Each
+  includes:
+
+  * barcode
+  * due_date
+
+-->
+<div>
+  <div>[% l('You renewed the following [_1] items:', '{{transactions.length}}') %]</div>
+  <hr/>
+  <ol>
+    <li ng-repeat="checkout in transactions">
+      <div>[% l('Barcode: [_1] Due: [_2]', 
+        '{{checkout.barcode}}',
+        '{{checkout.due_date | date:"short"}}') %]</div>
+    </li>
+  </ol>
+  <hr/>
+  <div>{{today | date:'short'}}</div>
+<br/>
+
diff --git a/Open-ILS/src/templates/staff/share/t_datetime.tt2 b/Open-ILS/src/templates/staff/share/t_datetime.tt2
index cc0afbf..6359212 100644
--- a/Open-ILS/src/templates/staff/share/t_datetime.tt2
+++ b/Open-ILS/src/templates/staff/share/t_datetime.tt2
@@ -3,10 +3,12 @@
     <!-- Date Picker -->
     <div class="input-group">
       <input type="text"
+        id="{{id}}"
         class="form-control"
         ng-show="!hideDatePicker"
         uib-datepicker-popup="{{date_format}}"
         is-open="datePickerIsOpen"
+        datepicker-options="options"
         ng-model="ngModel"
         ng-change="ngChange"
         ng-blur="ngBlur"
@@ -36,5 +38,9 @@
       </uib-timepicker>
     </span>
   </div>
+
+  <div>
+    <span ng-show="outOfRange" class="label label-danger">[% l('Input is out of range.') %]</span>
+  </div>
 </div>
 
diff --git a/Open-ILS/src/templates/staff/t_login.tt2 b/Open-ILS/src/templates/staff/t_login.tt2
index ce06e6b0..e4d42cb 100644
--- a/Open-ILS/src/templates/staff/t_login.tt2
+++ b/Open-ILS/src/templates/staff/t_login.tt2
@@ -49,6 +49,12 @@
               </div>
             </div>
 
+            <div class="form-group">
+              <div class="col-md-offset-4 col-md-6">
+                <span ng-show="pendingXacts" class="label label-warning">[% l('Unprocessed offline transactions waiting for upload') %]</span>
+              </div>
+            </div>
+
           </form>
         </fieldset>
       </div>
diff --git a/Open-ILS/web/LICENSE.UpUp b/Open-ILS/web/LICENSE.UpUp
new file mode 100644
index 0000000..c7fa58d
--- /dev/null
+++ b/Open-ILS/web/LICENSE.UpUp
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Tal Ater
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Open-ILS/web/js/ui/default/staff/Gruntfile.js b/Open-ILS/web/js/ui/default/staff/Gruntfile.js
index 8a885a1..d8af904 100644
--- a/Open-ILS/web/js/ui/default/staff/Gruntfile.js
+++ b/Open-ILS/web/js/ui/default/staff/Gruntfile.js
@@ -37,6 +37,7 @@ module.exports = function(grunt) {
             'node_modules/iframe-resizer/js/iframeResizer.map',
             'node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js',
             'node_modules/angular-order-object-by/src/ng-order-object-by.js',
+            'node_modules/angular-tablesort/js/angular-tablesort.js',
             'node_modules/lovefield/dist/lovefield.min.js',
             'node_modules/lovefield/dist/lovefield.min.js.map',
             'node_modules/moment/min/moment-with-locales.min.js',
@@ -58,6 +59,7 @@ module.exports = function(grunt) {
             'node_modules/ngtoast/dist/ngToast-animations.min.css',
             'node_modules/angular-tree-control/css/tree-control.css',
             'node_modules/angular-tree-control/css/tree-control-attribute.css',
+            'node_modules/angular-tablesort/tablesort.css'
           ]
         }]
       },
@@ -72,7 +74,8 @@ module.exports = function(grunt) {
             'node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.eot',
             'node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.svg',
             'node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf',
-            'node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff'
+            'node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff',
+            'node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2'
           ]
         }]
       },
@@ -110,7 +113,7 @@ module.exports = function(grunt) {
             'build/css/ngToast.min.css',
             'build/css/ngToast-animations.min.css',
             'build/css/tree-control.css',
-            'build/css/tree-control-attribute.css',
+            'build/css/tree-control-attribute.css'
           ]
         }
       }
@@ -130,7 +133,7 @@ module.exports = function(grunt) {
           rename: function (dst, src) {
             return src.replace('.js', '.min.js');
           }
-        }],
+        }]
       },
       build: {
         src: [
@@ -172,6 +175,7 @@ module.exports = function(grunt) {
             'services/ui.js',
             'services/date.js',
             'services/op_change.js',
+            'services/file.js'
         ],
         dest: 'build/js/<%= pkg.name %>.<%= pkg.version %>.min.js'
       },
@@ -181,7 +185,7 @@ module.exports = function(grunt) {
     // to more easily detect if concat order is incorrect
     concat: {
       options: {
-       separator: ';',
+       separator: ';'
       }
     },
 
@@ -190,19 +194,19 @@ module.exports = function(grunt) {
       // Generate test/data/IDL2js.js for unit tests.
       // note: the output of this script is *not* part of the final build.
       idl2js : {
-        command : 'cd test/data && perl idl2js.pl',
+        command : 'cd test/data && perl idl2js.pl'
       },
 
       // Remove the unit test IDL2js.js file.  We don't need it after testing
       rmidl2js : {
-        command : 'rm test/data/IDL2js.js',
+        command : 'rm test/data/IDL2js.js'
       }
     },
 
     // unit tests configuration
     karma : {
       unit: {
-        configFile: 'test/karma.conf.js',
+        configFile: 'test/karma.conf.js'
         //background: true  // for now, visually babysit unit tests
       }
     }
diff --git a/Open-ILS/web/js/ui/default/staff/app.js b/Open-ILS/web/js/ui/default/staff/app.js
index 4cf388c..f76c278 100644
--- a/Open-ILS/web/js/ui/default/staff/app.js
+++ b/Open-ILS/web/js/ui/default/staff/app.js
@@ -40,8 +40,12 @@ function($routeProvider , $locationProvider) {
     /* inject services into our controller.  Spelling them
      * out like this allows the auto-magic injector to work
      * even if the code has been minified */
-           ['$scope','$location','$window','egCore',
-    function($scope , $location , $window , egCore) {
+           ['$scope','$location','$window','egCore','egLovefield',
+    function($scope , $location , $window , egCore , egLovefield) {
+        egLovefield.havePendingOfflineXacts() .then(function(eh){
+            $scope.pendingXacts = eh;
+        });
+
         $scope.focusMe = true;
         $scope.args = {};
         $scope.workstations = [];
diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js b/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
index f40a9c0..6d9ddc8 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
@@ -2,7 +2,7 @@
 angular.module('egCoreMod')
 // toss tihs onto egCoreMod since the page app may vary
 
-.factory('patronRegSvc', ['$q', 'egCore', function($q, egCore) {
+.factory('patronRegSvc', ['$q', 'egCore', 'egLovefield', function($q, egCore, egLovefield) {
 
     var service = {
         field_doc : {},            // config.idl_field_doc
@@ -251,6 +251,11 @@ angular.module('egCoreMod')
                     });
                 });
             });
+
+            egLovefield.setListInOfflineCache('asv', service.surveys)
+            egLovefield.setListInOfflineCache('asvq', service.survey_questions)
+            egLovefield.setListInOfflineCache('asva', service.survey_answers)
+
         });
     }
 
@@ -270,6 +275,7 @@ angular.module('egCoreMod')
                 );
             });
             service.stat_cats = cats;
+            return egLovefield.setStatCatsCache(cats);
         });
     };
 
@@ -364,7 +370,7 @@ angular.module('egCoreMod')
     // some org settings require the retrieval of additional data
     service.process_org_settings = function(settings) {
 
-        var promises = [];
+        var promises = [egLovefield.setSettingsCache(settings)];
 
         if (settings['sms.enable']) {
             // fetch SMS carriers
@@ -434,14 +440,23 @@ angular.module('egCoreMod')
     }
 
     service.get_field_doc = function() {
+        var to_cache = [];
         return egCore.pcrud.search('fdoc', {
             fm_class: ['au', 'ac', 'aua', 'actsc', 'asv', 'asvq', 'asva']})
-        .then(null, null, function(doc) {
-            if (!service.field_doc[doc.fm_class()]) {
-                service.field_doc[doc.fm_class()] = {};
+        .then(
+            function () {
+                return egLovefield.setListInOfflineCache('fdoc', to_cache)
+            },
+            null,
+            function(doc) {
+                if (!service.field_doc[doc.fm_class()]) {
+                    service.field_doc[doc.fm_class()] = {};
+                }
+                service.field_doc[doc.fm_class()][doc.field()] = doc;
+                to_cache.push(doc);
             }
-            service.field_doc[doc.fm_class()][doc.field()] = doc;
-        });
+        );
+
     };
 
     service.get_user_settings = function() {
@@ -471,6 +486,8 @@ angular.module('egCoreMod')
             ]
         }, {}, {atomic : true}).then(function(setting_types) {
 
+            egCore.env.absorbList(setting_types, 'cust'); // why not...
+
             angular.forEach(setting_types, function(stype) {
                 service.user_setting_types[stype.name()] = stype;
                 if (static_types.indexOf(stype.name()) == -1) {
diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js b/Open-ILS/web/js/ui/default/staff/offline.js
similarity index 57%
copy from Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
copy to Open-ILS/web/js/ui/default/staff/offline.js
index f40a9c0..dab4065 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
+++ b/Open-ILS/web/js/ui/default/staff/offline.js
@@ -1,10 +1,730 @@
+/**
+ * App to drive the offline UI
+ */
 
-angular.module('egCoreMod')
-// toss tihs onto egCoreMod since the page app may vary
+lf.isOffline = true;
 
-.factory('patronRegSvc', ['$q', 'egCore', function($q, egCore) {
+angular.module('egOffline', ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'ngToast', 'tableSort'])
+
+.config(
+       ['$routeProvider','$locationProvider','$compileProvider',
+function($routeProvider , $locationProvider , $compileProvider) {
+
+    $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/);
+
+    /**
+     * Route resolvers allow us to run async commands
+     * before the page controller is instantiated.
+     */
+    var resolver = {delay : ['egCore', 
+        function(egCore) {
+            return egCore.startup.go();
+        }
+    ]};
+
+    $routeProvider.when('/offline-interface/:tab', {
+        templateUrl: 'offline-template',
+        controller: 'OfflineCtrl',
+        resolve : resolver
+    });
+
+    // default page 
+    $routeProvider.otherwise({
+        templateUrl : 'offline-template',
+        controller : 'OfflineCtrl',
+        resolve : resolver
+    });
+}])
+
+.controller('OfflineSessionCtrl', 
+           ['$scope','$window','egCore','$routeParams','$http','$q','$timeout','egPromptDialog','ngToast','egProgressDialog',
+    function($scope , $window , egCore , $routeParams , $http , $q , $timeout , egPromptDialog , ngToast , egProgressDialog) {
+        $scope.active_session_tab = 'pending';
+
+        $scope.lookupNoncatTypeName = function (type) {
+            var nc =  $scope.noncats.filter(function(n){ return n.id() == type })[0];
+            if (nc) return nc.name();
+            return '';
+        }
+
+        $scope.createDate = function (ts, epoch) {
+            if (!ts) return '';
+            if (epoch) ts = ts * 1000;
+            return new Date(ts);
+        }
+
+        $scope.setSession = function (s, ind) {
+            $scope.current_session = s;
+            $scope.current_session_index = ind;
+
+            return $scope.refreshExceptions(s);
+        }
+
+        $scope.createSession = function () {
+
+            return egPromptDialog.open(
+                egCore.strings.OFFLINE_SESSION_DESC, '',
+                {ok : function(value) {
+                    if (value) {
+
+                        return $http.get(formURL({action:'create',desc:value})).then(function(res) {
+                            if (res.data.ilsevent == "0") return $q.when(res.data.payload);
+                            return $q.reject();
+                        }).then(function (seskey) {
+                            return $scope.refreshSessions().then(function() {
+                                if (seskey) {
+                                    var s = $scope.sessions.filter(function(s){ s.key == seskey })[0];
+                                    var ind = $scope.sessions.length - 1; // sorted by create time, so new one is last
+                                    return $scope.setSession(s, ind);
+                                }
+                            });
+                        }, function() {
+                            ngToast.warning(egCore.strings.OFFLINE_SESSION_CREATE_FAILED);
+                        });
+                    }
+                }}
+            );
+        }
+
+        $scope.processSession = function (s, ind) {
+            return $scope.setSession(s, ind).then(function() {
+                egProgressDialog.open();
+
+                return $http.get(
+                    formURL({action:'execute',seskey:$scope.current_session.key})
+                ).then(function(res) {
+                    if (res.data.ilsevent == "0") return $q.when(res.data.payload);
+                    return $q.reject();
+                }).then(function () {
+                    egProgressDialog.close();
+                    return $scope.refreshSessions()
+                        .then(function(){ return $scope.refreshExceptions(s) });
+                },function () {
+                    egProgressDialog.close();
+                    return $scope.refreshSessions().then(function() {
+                        ngToast.warning(egCore.strings.OFFLINE_SESSION_PROCESSING_FAILED);
+                    });
+                });
+            });
+        }
+
+        $scope.refreshExceptions = function (s) {
+            return $http.get(
+                formURL({
+                    action      : 'status',
+                    status_type : 'exceptions',
+                    seskey      : s.key
+                })
+            ).then(function(res) {
+                if (res.data.ilsevent) {
+                    $scope.current_session.exceptions = [];
+                } else {
+                    $scope.current_session.exceptions = res.data;
+                }
+                return $q.when();
+            });
+        }
+
+        $scope.refreshSessions = function () {
+
+            return $http.get(formURL({action:'status',status_type:'sessions'})).then(function(res) {
+                if (res.data) {
+                    $scope.sessions = res.data;
+                    return $q.when();
+                }
+                return $q.reject();
+            }).then(function() {
+                var creator_list = [$q.when()];
+                angular.forEach($scope.sessions, function (s) {
+                    s.total = 0;
+                    s.org = egCore.org.get(s.org).shortname();
+                    creator_list.push(egCore.pcrud.retrieve('au',s.creator).then(function(u) {
+                        s.creator = u.family_name();
+                    }));
+                    angular.forEach(s.scripts, function(sc) {
+                        s.total += sc.count;
+                    });
+                });
+
+                return $q.all(creator_list);
+            });
+        }
+
+        $scope.reprintLast = function () {
+            egCore.print.reprintLast();
+        }
+
+
+        $scope.uploadPending = function (s, ind) {
+            return $scope.setSession(s, ind).then(function() {
+
+                egProgressDialog.open();
+                return $scope.createOfflineXactBlob().then(function(blob) {
+
+                    var form = new FormData();
+                    form.append("ses", egCore.auth.token());
+                    form.append("org", $scope.org.id());
+                    form.append("ws", $scope.current_workstation_name());
+                    form.append("wc", 1);
+                    form.append("action", "load");
+                    form.append("seskey", $scope.current_session.key);
+                    form.append("file", blob, "file");
+
+                    return $http.post(
+                        '/cgi-bin/offline/offline.pl?' + new Date().getTime(),
+                        form,
+                        {
+                            transformRequest: angular.identity,
+                            headers: {'Content-Type': undefined}
+                        }
+                    ).then(function(res) {
+                        egProgressDialog.close();
+                        if (res.data.ilsevent == "0") {
+                            return $scope.clear_pending(true).then(function() {
+                                return $scope.refreshSessions();
+                            });
+                        } else {
+                            ngToast.warning(egCore.strings.OFFLINE_SESSION_UPLOAD_FAILED);
+                            return $scope.refreshSessions();
+                        }
+                    },function () { egProgressDialog.close() });
+                });
+            });
+        }
+
+        $scope.retrieveDetails = function (x) {
+            alert(JSON.stringify(x, null, 2)); // egAlertDialog kills pretty printing
+        }
+
+        $scope.retrieveItem = function (bc) {
+            return egCore.pcrud.search('acp',{deleted: 'f', barcode: bc}).then(function(copy) {
+                if (copy) {
+                    return $window.open(
+                        egCore.env.basePath +
+                        '/cat/item/' + copy.id(),
+                        '_blank'
+                    ).focus();
+                }
+
+                ngToast.warning(egCore.strings.ITEM_NOT_FOUND);
+            });
+        }
+
+        $scope.retrievePatron = function (bc) {
+            return egCore.pcrud.search('ac',{barcode: bc}).then(function(card) {
+                if (card) {
+                    return $window.open(
+                        egCore.env.basePath +
+                        '/circ/patron/' + card.usr() + '/checkout',
+                        '_blank'
+                    ).focus();
+                }
+
+                ngToast.warning(egCore.strings.PATRON_NOT_FOUND);
+            });
+        }
+
+        function formURL (params) {
+            var url = '/cgi-bin/offline/offline.pl?' + new Date().getTime();
+
+            var defaults = {
+                org : $scope.org ? $scope.org.id() : null,
+                ws  : $scope.current_workstation_name(),
+                wc  : 1,
+                ses : egCore.auth.token()
+            }
+
+            angular.extend(params, defaults)
+
+            var first = true;
+            for (var k in params) {
+                url += '&' + k + '=' + window.encodeURIComponent(params[k]);
+            }
+            return url;
+        }
+
+        $scope.$watch('org',function(n){if (n) $scope.refreshSessions()});
+
+    }
+])
+
+.controller('OfflineCtrl', 
+           ['$q','$scope','$window','$location','$rootScope','egCore','egLovefield','$routeParams','$timeout','$http','ngToast','egConfirmDialog','egUnloadPrompt',
+    function($q , $scope , $window , $location , $rootScope , egCore , egLovefield , $routeParams , $timeout , $http , ngToast , egConfirmDialog , egUnloadPrompt) {
+        $scope.active_tab = $routeParams.tab || 'checkout';
+
+        // Immediately redirect if we're really offline
+        if (!$window.navigator.onLine) {
+            if ($location.path().match(/session$/)) {
+                var path = $location.path();
+                return $location.path(path.replace('session','checkout'));
+            }
+        }
+
+        var today = new Date();
+        today.setHours(0);
+        today.setMinutes(0);
+        today.setSeconds(0);
+        today.setMilliseconds(0);
+
+        $scope.minDate = today;
+        $scope.blocked_patron = null;
+        $scope.bad_barcode = null;
+        $scope.barcode_type = 'barcode';
+        $scope.focusMe = true;
+        $scope.shared = { outOfRange : false, due_date : null, due_date_offset : '' };
+        $scope.workstation_obj = null;
+        $scope.workstation = '';
+        $scope.workstation_owner = '';
+        $scope.workstations = [];
+        $scope.org = null;
+        $scope.do_print = Boolean($scope.active_tab == 'checkout');
+        $scope.do_print_changed = false;
+        $scope.printed = false;
+
+        $scope.imported_pending_xacts = { data : '' };
+
+        $scope.xact_page = { checkin:[], checkout:[], renew:[], in_house_use:[] };
+        $scope.all_xact = [];
+        $scope.noncats = [];
+
+        $scope.checkout = { noncat_type : '' };
+        $scope.renew = { noncat_type : '' };
+        $scope.in_house_use = {count : 1};
+        $scope.checkin = { backdate : new Date() };
+
+        $scope.current_workstation_owning_lib = function () {
+            return $scope.workstations.filter(function(w) {
+                return $scope.workstation == w.id
+            })[0].owning_lib;
+        }
+
+        $scope.current_workstation_name = function () {
+            return $scope.workstations.filter(function(w) {
+                return $scope.workstation == w.id
+            })[0].name;
+        }
+
+        $scope.$watch('workstation', function (n,o) {
+            if (egCore.env.aou)
+                $scope.org = egCore.org.get($scope.current_workstation_owning_lib());
+        });
+
+        $scope.changeCheck = function () {
+            $scope.strict_barcode = !$scope.strict_barcode;
+            $scope.do_check_changed = true;
+            egCore.hatch.setItem('eg.offline.strict_barcode', $scope.strict_barcode)
+        }
+
+        $scope.changePrint = function () {
+            $scope.do_print = !$scope.do_print;
+            $scope.do_print_changed = true;
+            egCore.hatch.setItem('eg.offline.print_receipt', $scope.do_print)
+        }
+
+        $scope.logged_in = egCore.auth.token() ? true : false;
+
+        if (!$scope.logged_in && $routeParams.tab == 'session')
+            $scope.active_tab = 'checkout';
+        
+        egCore.hatch.getItem('eg.offline.print_receipt')
+        .then(function(setting) {
+            $scope.do_print = setting;
+            if (setting !== undefined) $scope.do_print_changed = true;
+        });
+
+        egCore.hatch.getItem('eg.offline.strict_barcode')
+        .then(function(setting) {
+            $scope.strict_barcode = setting;
+            if (setting !== undefined) $scope.do_check_changed = true;
+        });
+
+        egCore.hatch.getItem('eg.workstation.all')
+        .then(function(all) {
+            if (all && all.length) {
+                $scope.workstations = all;
+
+                if (ws = $location.search().ws) {
+                    // user requested a workstation via URL
+                    var match = all.filter(
+                        function(w) {return ws == w.name} )[0];
+
+                    if (match) {
+                        // requested WS registered on this client
+                        $scope.workstation = match.id;
+                    } else {
+                        // the requested WS is not registered on this client
+                        $scope.wsNotRegistered = true;
+                    }
+                } else {
+                    // no workstation requested; use the default
+                    egCore.hatch.getItem('eg.workstation.default')
+                    .then(function(ws) {
+                        var ws_obj = all.filter(function(w) {
+                            return ws == w.name
+                        })[0];
+
+                        $scope.workstation_obj = ws_obj;
+                        $scope.workstation = ws_obj.id;
+                        $scope.workstation_owner = ws_obj.owning_lib;
+
+                        return egLovefield.reconstituteList('cnct').then(function () {
+                            $scope.noncats = egCore.env.cnct.list;
+                        });
+                    });
+                }
+            } 
+        });
+
+        $scope.downloadBlockList = function () {
+            var url = '/standalone/list.txt?ses='
+                + egCore.auth.token()
+                + '&' + new Date().getTime();
+            return $http.get(url).then(
+                function (res) {
+                    if (res.data) {
+                        var lines = res.data.split('\n');
+                        egLovefield.destroyOfflineBlocks().then(function(){
+                            angular.forEach(lines, function (l) {
+                                var parts = l.split(' ');
+                                egLovefield.addOfflineBlock(parts[0], parts[1]);
+                            });
+                            return $q.when();
+                        }).then(function(){
+                            ngToast.create(egCore.strings.OFFLINE_BLOCKLIST_SUCCESS);
+                        });
+                    }
+                },function(){
+                    ngToast.warning(egCore.strings.OFFLINE_BLOCKLIST_FAIL);
+                    egCore.audio.play('warning.offline.blocklist_fail');
+                }
+            );
+        }
+
+        $scope.createOfflineXactBlob = function () {
+            return egLovefield.retrievePendingOfflineXacts().then(function(list) {
+                var flat_list = [];
+                angular.forEach(list, function (i) {
+                    flat_list.push(JSON.stringify(i) + '\n');
+                });
+
+                var blob = new Blob(flat_list, {type: 'text/plain'});
+
+                return $q.when(blob)
+            });
+        }
+
+        $scope.pending_xacts = [];
+        $scope.retrieve_pending = function () {
+            return egLovefield.retrievePendingOfflineXacts().then(function(list) {
+                $scope.pending_xacts = list;
+                return $q.when(list);
+            });
+        }
+
+        $scope.save = function () {
+            var promises = [$q.when()];
+            angular.forEach($scope.all_xact, function (x) {
+                promises.push(egLovefield.addOfflineXact(x));
+            });
+
+            var prints = [$q.when()];
+            if ($scope.do_print) {
+                angular.forEach(['checkin','checkout','renew','in_house_use'], function(xtype) {
+                    if ($scope.xact_page[xtype].length > 0) {
+                        prints.push(egCore.print.print({
+                            context : 'offline', 
+                            template : 'offline_'+xtype,
+                            scope : {
+                                transactions    : $scope.xact_page[xtype]
+                            }
+                        }));
+                    }
+                });
+            }
+
+            return $q.all(promises.concat(prints)).finally(function() {
+                egUnloadPrompt.clear();
+                if (prints.length > 1) $scope.printed = true;
+                $scope.all_xact = [];
+                $scope.xact_page = { checkin:[], checkout:[], renew:[], in_house_use:[] };
+                angular.forEach(['checkout','renew'], function (xtype) {
+                    $scope[xtype].patron_barcode = '';
+                });
+                $scope.retrieve_pending();
+            });
+        }
+
+        $rootScope.save_offline_xacts = function () { return $scope.save() };
+        $rootScope.active_tab = function (t) { $scope.active_tab = t };
+
+        $scope.logout = function () {
+            egCore.auth.logout();
+            $window.location.href = location.href;
+        }
+
+        $scope.clear_pending = function (skip_confirm) {
+            if (skip_confirm) {
+                return egLovefield.destroyPendingOfflineXacts().then(function () {
+                    return $scope.retrieve_pending();
+                });
+            }
+            return egConfirmDialog.open(
+                egCore.strings.CONFIRM_CLEAR_PENDING,
+                egCore.strings.CONFIRM_CLEAR_PENDING_BODY,
+                {}
+            ).result.then(function() {
+                return egLovefield.destroyPendingOfflineXacts().then(function () {
+                    return $scope.retrieve_pending();
+                });
+            });
+
+        }
+
+        $scope.retrieve_pending();
+        $scope.$watch('active_tab', function (n,o) {
+            if (n != o && !$scope.do_check_changed && n != 'checkout') $scope.strict_barcode = false;
+            if (n != o && !$scope.do_check_changed && n == 'checkout') $scope.strict_barcode = true;
+            if (n != o && !$scope.do_print_changed && n != 'checkout') $scope.do_print = false;
+            if (n != o && !$scope.do_print_changed && n == 'checkout') $scope.do_print = true;
+            if (n != o && n == 'session') $scope.retrieve_pending();
+        });
+
+        $scope.$watch('imported_pending_xacts.data', function (n, o) {
+            if (n != 0) {
+                var lines = n.split('\n');
+                var promises = [];
+
+                angular.forEach(lines, function (l) {
+                    if (!l) return;
+
+                    try {
+                        promises.push(
+                            egLovefield.addOfflineXact(JSON.parse(l))
+                        );
+                    } catch (err) {
+                        ngToast.warning(err);
+                    }
+                });
+
+                $q.all(promises).then(function () { $scope.retrieve_pending() });
+            }
+        });
+
+        $scope.resetDueDate = function (xtype) {
+            $scope.shared.due_date = new Date();
+            $scope.shared.due_date.setDate($scope.shared.due_date.getDate() + parseInt($scope.shared.due_date_offset));
+        }
+
+        $scope.notEnough = function (xtype) {
+
+            if (xtype == 'checkout') {
+                if ($scope.shared.outOfRange) return true;
+                if (
+                    $scope.checkout.patron_barcode &&
+                    ($scope.shared.due_date || $scope.shared.due_date_offset) &&
+                    ($scope.checkout.barcode || ($scope.checkout.noncat_type && $scope.checkout.noncat_count))
+                ) return false;
+                return true;
+            }
+
+            if (xtype == 'renew') {
+                if ($scope.shared.outOfRange) return true;
+                if (
+                    $scope.renew.barcode &&
+                    ($scope.shared.due_date || $scope.shared.due_date_offset)
+                ) return false;
+                return true;
+            }
+
+            if (xtype == 'in_house_use') {
+                if (
+                    $scope.in_house_use.barcode && $scope.in_house_use.count
+                ) return false;
+                return true;
+            }
+
+            if (xtype == 'checkin') {
+                if (
+                    $scope.checkin.barcode && $scope.checkin.backdate
+                ) return false;
+                return true;
+            }
+        }
+
+        $scope.clear = function (xtype) {
+            $scope[xtype] = {};
+            if (xtype=="in_house_use") $scope[xtype].count = 1;
+        }
+
+        $scope.add = function (xtype,next_focus) {
+
+            var barcode = $scope[xtype].barcode;
+            if (barcode) {
+                if ($scope.xact_page[xtype].filter(function(x){ return x.barcode == barcode }).length > 0) {
+                    ngToast.warning(egCore.strings.DUPLICATE_BARCODE);
+                    egCore.audio.play('warning.offline.duplicate_barcode');
+                    $scope[xtype].barcode = '';
+                    if (next_focus) $('#'+next_focus).focus();
+                    return;
+                }
+            }
+
+            var pbarcode = $scope[xtype].patron_barcode;
+            if (pbarcode) {
+                egLovefield.testOfflineBlock(pbarcode).then(function (blocked) {
+                    if (blocked) {
+                        egCore.audio.play('warning.offline.blocked_patron');
+                        egConfirmDialog.open(
+                            egCore.strings.PATRON_BLOCKED,
+                            egCore.strings.PATRON_BLOCKED_WHY[blocked],
+                            {}, egCore.strings.ALLOW, egCore.strings.REJECT
+                        ).result.then(
+                            function(){ // forced
+                                $scope.blocked_patron = null;
+                                _add_impl(xtype,true)
+                                if (next_focus) $('#'+next_focus).focus();
+                            },function(){ // stopped
+                                $scope.blocked_patron = xtype;
+                                if (next_focus) $('#'+next_focus).focus();
+                                return;
+                            }
+                        );
+                    } else {
+                        $scope.blocked_patron = null;
+                        _add_impl(xtype,true)
+                        if (next_focus) $('#'+next_focus).focus();
+                    }
+                });
+            } else {
+                _add_impl(xtype);
+                if (next_focus) $('#'+next_focus).focus();
+            }
+        }
+
+        function _add_impl (xtype,digest) {
+            var pbarcode = $scope[xtype].patron_barcode;
+            var backdate = $scope[xtype].backdate;
+
+            if ($scope.strict_barcode && pbarcode) {
+                if (!check_barcode(pbarcode)) {
+                    $scope.bad_barcode = xtype;
+                    egCore.audio.play('warning.offline.bad_barcode');
+                    return egConfirmDialog.open(
+                        egCore.strings.BAD_PATRON_BARCODE,
+                        egCore.strings.BAD_PATRON_BARCODE_CD,
+                        {}, egCore.strings.ALLOW, egCore.strings.REJECT
+                    ).result.then(
+                        function(){ // forced
+                            $scope.blocked_patron = null;
+                            return _add_impl2(xtype,digest)
+                        },function(){ // stopped
+                            $scope.blocked_patron = xtype;
+                        }
+                    );
+                }
+            }
+
+            if ($scope.strict_barcode && $scope[xtype].barcode) {
+                if (!check_barcode($scope[xtype].barcode)) {
+                    $scope.bad_barcode = xtype;
+                    egCore.audio.play('warning.offline.bad_barcode');
+                    return egConfirmDialog.open(
+                        egCore.strings.BAD_BARCODE,
+                        egCore.strings.BAD_BARCODE_CD,
+                        {}, egCore.strings.ALLOW, egCore.strings.REJECT
+                    ).result.then(
+                        function(){ // forced
+                            $scope.blocked_patron = null;
+                            return _add_impl2(xtype,digest)
+                        },function(){ // stopped
+                            $scope.blocked_patron = xtype;
+                        }
+                    );
+                }
+            }
+
+            return _add_impl2(xtype,digest);
+        }
+
+        function _add_impl2 (xtype,digest) {
+            var pbarcode = $scope[xtype].patron_barcode;
+            var backdate = $scope[xtype].backdate;
+
+            $scope.bad_barcode = null;
+
+            var now = new Date().getTime();
+            now = now / 1000;
+
+            if ($scope[xtype].noncat_type) $scope[xtype].noncat = 1;
+
+            if ($scope.shared.due_date && (xtype == 'checkout' || xtype == 'renew')) {
+                $scope[xtype].due_date = $scope.shared.due_date.toISOString();
+                $scope[xtype].checkout_time = new Date().toISOString();
+            }
+
+            var xact = { timestamp : parseInt(now), type : xtype, delta : 0 };
+
+            $scope.xact_page[xtype].push(
+                angular.extend(xact, $scope[xtype])
+            );
+
+            $scope.all_xact.push(xact)
+            egUnloadPrompt.attach($rootScope);
+
+            $scope[xtype] = {};
+
+            if (pbarcode) $scope[xtype].patron_barcode = pbarcode;
+            if (backdate) $scope[xtype].backdate = backdate;
+            if (xtype=="in_house_use") $scope[xtype].count = 1;
+
+            if (digest) $timeout(function(){$scope.$apply()});
+        }
+
+        check_barcode = function(bc) {
+            if (bc != Number(bc)) return false;
+            bc = bc.toString();
+            // "16.00" == Number("16.00"), but the . is bad.
+            // Throw out any barcode that isn't just digits
+            if (bc.search(/\D/) != -1) return false;
+            var last_digit = bc.substr(bc.length-1);
+            var stripped_barcode = bc.substr(0,bc.length-1);
+            return barcode_checkdigit(stripped_barcode).toString() == last_digit;
+        }
+    
+        barcode_checkdigit = function(bc) {
+            var reverse_barcode = bc.toString().split('').reverse();
+            var check_sum = 0; var multiplier = 2;
+            for (var i = 0; i < reverse_barcode.length; i++) {
+                var digit = reverse_barcode[i];
+                var product = digit * multiplier; product = product.toString();
+                var temp_sum = 0;
+                for (var j = 0; j < product.length; j++) {
+                    temp_sum += Number( product[j] );
+                }
+                check_sum += Number( temp_sum );
+                multiplier = ( multiplier == 2 ? 1 : 2 );
+            }
+            check_sum = check_sum.toString();
+            var next_multiple_of_10 = (check_sum.match(/(\d*)\d$/)[1] * 10) + 10;
+            var check_digit = next_multiple_of_10 - Number(check_sum);
+            if (check_digit == 10) check_digit = 0;
+            return check_digit;
+        }
+
+    }
+])
+
+// dummy service so standalone patron editor can reference it
+.factory('patronSvc', function() { return { /* dummy */ } })
+
+.factory('patronRegSvc', ['$q', 'egCore', 'egLovefield', function($q, egCore, egLovefield) {
+
+    egLovefield.isOffline = true;
 
     var service = {
+        org : null,                // will come from workstation org 
         field_doc : {},            // config.idl_field_doc
         profiles : [],             // permission groups
         edit_profiles : [],        // perm groups we can modify
@@ -22,6 +742,10 @@ angular.module('egCoreMod')
         init_done : false           // have we loaded our initialization data?
     };
 
+    service.offlineMode = function () {
+        return lf.isOffline;
+    }
+
     // launch a series of parallel data retrieval calls
     service.init = function(scope) {
 
@@ -41,98 +765,16 @@ angular.module('egCoreMod')
             service.get_org_settings(),
             service.get_stat_cats(),
             service.get_surveys(),
-            service.get_clone_user(),
-            service.get_stage_user(),
             service.get_net_access_levels()
         ]);
     };
 
-    service.get_clone_user = function() {
-        if (!service.clone_id) return $q.when();
-        // we could load egUser and use its get() function, but loading
-        // user.js into the standalone register UI would mean creating a
-        // new module, since egUser is not loaded into egCoreMod.  This
-        // is a lot simpler.
-        return egCore.net.request(
-            'open-ils.actor',
-            'open-ils.actor.user.fleshed.retrieve',
-            egCore.auth.token(), service.clone_id, 
-            ['billing_address', 'mailing_address'])
-        .then(function(cuser) {
-            if (e = egCore.evt.parse(cuser)) {
-                alert(e);
-            } else {
-                service.clone_user = cuser;
-            }
-        });
-    }
-
-    // When editing a user with addresses linked to other users, fetch
-    // the linked user(s) so we can display their names and edit links.
     service.get_linked_addr_users = function(addrs) {
-        angular.forEach(addrs, function(addr) {
-            if (addr.usr == service.existing_patron.id()) return;
-            egCore.pcrud.retrieve('au', addr.usr)
-            .then(function(usr) {
-                addr._linked_owner_id = usr.id();
-                addr._linked_owner = service.format_name(
-                    usr.family_name(),
-                    usr.first_given_name(),
-                    usr.second_given_name()
-                );
-            })
-        });
+        return $q.when();
     }
 
     service.apply_secondary_groups = function(user_id, group_ids) {
-        return egCore.net.request(
-            'open-ils.actor',
-            'open-ils.actor.user.set_groups',
-            egCore.auth.token(), user_id, group_ids)
-        .then(function(resp) {
-            if (resp == 1) {
-                return true;
-            } else {
-                // debugging -- should be no events
-                alert('linked groups failure ' + egCore.evt.parse(resp));
-            }
-        });
-    }
-
-    service.get_stage_user = function() {
-        if (!service.stage_username) return $q.when();
-
-        // fetch the staged user object
-        return egCore.net.request(
-            'open-ils.actor',
-            'open-ils.actor.user.stage.retrieve.by_username',
-            egCore.auth.token(), 
-            service.stage_username
-        ).then(function(suser) {
-            if (e = egCore.evt.parse(suser)) {
-                alert(e);
-            } else {
-                service.stage_user = suser;
-            }
-        }).then(function() {
-
-            if (!service.stage_user) return;
-            var requestor = service.stage_user.user.requesting_usr();
-
-            if (!requestor) return;
-
-            // fetch the requesting user
-            return egCore.net.request(
-                'open-ils.actor', 
-                'open-ils.actor.user.retrieve.parts',
-                egCore.auth.token(),
-                requestor, 
-                ['family_name', 'first_given_name', 'second_given_name'] 
-            ).then(function(parts) {
-                service.stage_user_requestor = 
-                    service.format_name(parts[0], parts[1], parts[2]);
-            })
-        });
+        return $q.when(true);
     }
 
     // See note above about not loading egUser.
@@ -142,59 +784,15 @@ angular.module('egCoreMod')
     }
 
     service.check_dupe_username = function(usrname) {
-
-        // empty usernames can't be dupes
-        if (!usrname) return $q.when(false);
-
-        // avoid dupe check if username matches the originally loaded usrname
-        if (service.existing_patron) {
-            if (usrname == service.existing_patron.usrname())
-                return $q.when(false);
-        }
-
-        return egCore.net.request(
-            'open-ils.actor',
-            'open-ils.actor.username.exists',
-            egCore.auth.token(), usrname);
+        return $q.when(false);
     }
 
-    //service.check_grp_app_perm = function(grp_id) {
-
     // determine which user groups our user is not allowed to modify
     service.set_edit_profiles = function() {
-        var all_app_perms = [];
-        var failed_perms = [];
-
-        // extract the application permissions
-        angular.forEach(service.profiles, function(grp) {
-            if (grp.application_perm())
-                all_app_perms.push(grp.application_perm());
-        }); 
-
-        // fill in service.edit_profiles by inspecting failed_perms
-        function traverse_grp_tree(grp, failed) {
-            failed = failed || 
-                failed_perms.indexOf(grp.application_perm()) > -1;
-
-            if (!failed) service.edit_profiles.push(grp);
-
-            angular.forEach(
-                service.profiles.filter( // children of grp
-                    function(p) { return p.parent() == grp.id() }),
-                function(child) {traverse_grp_tree(child, failed)}
-            );
-        }
-
-        return egCore.perm.hasPermAt(all_app_perms, true).then(
-            function(perm_orgs) {
-                angular.forEach(all_app_perms, function(p) {
-                    if (perm_orgs[p].length == 0)
-                        failed_perms.push(p);
-                });
-
-                traverse_grp_tree(egCore.env.pgt.tree);
-            }
+        service.edit_profiles = egCore.env.pgt.list.filter(
+            function (p) { return p.application_perm() == 'group_application.user.patron' }
         );
+        return $q.when;
     }
 
     // resolves to a hash of perm-name => boolean value indicating
@@ -212,208 +810,84 @@ angular.module('egCoreMod')
             'UPDATE_PATRON_PRIMARY_CARD'
         ];
 
-        return egCore.perm.hasPermAt(perms_needed, true)
-        .then(function(perm_map) {
-
-            angular.forEach(perms_needed, function(perm) {
-                perm_map[perm] = 
-                    Boolean(perm_map[perm].indexOf(org_id) > -1);
-            });
-
-            return perm_map;
+        var hash = {};
+        angular.forEach(perms_needed, function (p) {
+            hash[p] = true;
         });
+
+        return $q.when(hash);
     }
 
     service.get_surveys = function() {
-        var org_ids = egCore.org.fullPath(egCore.auth.user().ws_ou(), true);
-
-        return egCore.pcrud.search('asv', {
-                owner : org_ids,
-                start_date : {'<=' : 'now'},
-                end_date : {'>=' : 'now'}
-            }, {   
-                flesh : 2, 
-                flesh_fields : {
-                    asv : ['questions'], 
-                    asvq : ['answers']
-                }
-            }, 
-            {atomic : true}
-        ).then(function(surveys) {
-            surveys = surveys.sort(function(a,b) {
-                return a.name() < b.name() ? -1 : 1 });
-            service.surveys = surveys;
-            angular.forEach(surveys, function(survey) {
-                angular.forEach(survey.questions(), function(question) {
-                    service.survey_questions[question.id()] = question;
-                    angular.forEach(question.answers(), function(answer) {
-                        service.survey_answers[answer.id()] = answer;
+        return egLovefield.reconstituteList('asv').then(function(offline) {
+            return egLovefield.reconstituteList('asvq')
+                    .then(function(){
+                        return egLovefield.reconstituteList('asva');
+                    }).then(function() {
+                        angular.forEach(egCore.env.asv.list, function (s) {
+                            s.questions( egCore.env.asva.list.filter( function (a) {
+                                return q.survey().id == s.id();
+                            }));
+                        });
+
+                        angular.forEach(egCore.env.asvq.list, function (q) {
+                            q.survey( egCore.env.asv.map[ q.survey().id ] );
+                            q.answers( egCore.env.asva.list.filter( function (a) {
+                                return q.id() == a.question();
+                            }));
+                        });
+
+                        angular.forEach(egCore.env.asva.list, function (a) {
+                            a.question( egCore.env.asvq.map[ a.question().id ] );
+                        });
+
+                        service.surveys = egCore.env.asv.list;
+                        service.survey_questions = egCore.env.asvq.list;
+                        service.survey_answers = egCore.env.asva.list;
+
+                        return $q.when();
                     });
-                });
-            });
         });
     }
 
     service.get_stat_cats = function() {
-        return egCore.net.request(
-            'open-ils.circ',
-            'open-ils.circ.stat_cat.actor.retrieve.all',
-            egCore.auth.token(), egCore.auth.user().ws_ou()
-        ).then(function(cats) {
-            cats = cats.sort(function(a, b) {
-                return a.name() < b.name() ? -1 : 1});
-            angular.forEach(cats, function(cat) {
-                cat.entries(
-                    cat.entries().sort(function(a,b) {
-                        return a.value() < b.value() ? -1 : 1
-                    })
-                );
-            });
-            service.stat_cats = cats;
-        });
+        return egLovefield.getStatCatsCache().then(
+            function(cats) {
+                service.stat_cats = cats;
+                return $q.when();
+            }
+        );
     };
 
     service.get_org_settings = function() {
-        return egCore.org.settings([
-            'global.password_regex',
-            'global.juvenile_age_threshold',
-            'patron.password.use_phone',
-            'ui.patron.default_inet_access_level',
-            'ui.patron.default_ident_type',
-            'ui.patron.default_country',
-            'ui.patron.registration.require_address',
-            'circ.holds.behind_desk_pickup_supported',
-            'circ.patron_edit.clone.copy_address',
-            'ui.patron.edit.au.prefix.require',
-            'ui.patron.edit.au.prefix.show',
-            'ui.patron.edit.au.prefix.suggest',
-            'ui.patron.edit.ac.barcode.regex',
-            'ui.patron.edit.au.second_given_name.show',
-            'ui.patron.edit.au.second_given_name.suggest',
-            'ui.patron.edit.au.suffix.show',
-            'ui.patron.edit.au.suffix.suggest',
-            'ui.patron.edit.au.alias.show',
-            'ui.patron.edit.au.alias.suggest',
-            'ui.patron.edit.au.dob.require',
-            'ui.patron.edit.au.dob.show',
-            'ui.patron.edit.au.dob.suggest',
-            'ui.patron.edit.au.dob.calendar',
-            'ui.patron.edit.au.juvenile.show',
-            'ui.patron.edit.au.juvenile.suggest',
-            'ui.patron.edit.au.ident_value.show',
-            'ui.patron.edit.au.ident_value.suggest',
-            'ui.patron.edit.au.ident_value2.show',
-            'ui.patron.edit.au.ident_value2.suggest',
-            'ui.patron.edit.au.email.require',
-            'ui.patron.edit.au.email.show',
-            'ui.patron.edit.au.email.suggest',
-            'ui.patron.edit.au.email.regex',
-            'ui.patron.edit.au.email.example',
-            'ui.patron.edit.au.day_phone.require',
-            'ui.patron.edit.au.day_phone.show',
-            'ui.patron.edit.au.day_phone.suggest',
-            'ui.patron.edit.au.day_phone.regex',
-            'ui.patron.edit.au.day_phone.example',
-            'ui.patron.edit.au.evening_phone.require',
-            'ui.patron.edit.au.evening_phone.show',
-            'ui.patron.edit.au.evening_phone.suggest',
-            'ui.patron.edit.au.evening_phone.regex',
-            'ui.patron.edit.au.evening_phone.example',
-            'ui.patron.edit.au.other_phone.require',
-            'ui.patron.edit.au.other_phone.show',
-            'ui.patron.edit.au.other_phone.suggest',
-            'ui.patron.edit.au.other_phone.regex',
-            'ui.patron.edit.au.other_phone.example',
-            'ui.patron.edit.phone.regex',
-            'ui.patron.edit.phone.example',
-            'ui.patron.edit.au.active.show',
-            'ui.patron.edit.au.active.suggest',
-            'ui.patron.edit.au.barred.show',
-            'ui.patron.edit.au.barred.suggest',
-            'ui.patron.edit.au.master_account.show',
-            'ui.patron.edit.au.master_account.suggest',
-            'ui.patron.edit.au.claims_returned_count.show',
-            'ui.patron.edit.au.claims_returned_count.suggest',
-            'ui.patron.edit.au.claims_never_checked_out_count.show',
-            'ui.patron.edit.au.claims_never_checked_out_count.suggest',
-            'ui.patron.edit.au.alert_message.show',
-            'ui.patron.edit.au.alert_message.suggest',
-            'ui.patron.edit.aua.post_code.regex',
-            'ui.patron.edit.aua.post_code.example',
-            'ui.patron.edit.aua.county.require',
-            'format.date',
-            'ui.patron.edit.default_suggested',
-            'opac.barcode_regex',
-            'opac.username_regex',
-            'sms.enable',
-            'ui.patron.edit.aua.state.require',
-            'ui.patron.edit.aua.state.suggest',
-            'ui.patron.edit.aua.state.show',
-            'ui.admin.work_log.max_entries',
-            'ui.admin.patron_log.max_entries'
-        ]).then(function(settings) {
-            service.org_settings = settings;
-            if (egCore && egCore.env && !egCore.env.aous) {
-                egCore.env.aous = settings;
-                console.log('setting egCore.env.aous');
+        return egLovefield.getSettingsCache().then(
+            function (list) {
+                var hash = {};
+                angular.forEach(list, function (s) {
+                    hash[s.name] = s.value;
+                });
+                service.org_settings = hash;
+                if (egCore && egCore.env && !egCore.env.aous) {
+                    egCore.env.aous = hash;
+                    console.log('setting egCore.env.aous');
+                }
+                return $q.when();
             }
-            return service.process_org_settings(settings);
-        });
-    };
-
-    // some org settings require the retrieval of additional data
-    service.process_org_settings = function(settings) {
-
-        var promises = [];
-
-        if (settings['sms.enable']) {
-            // fetch SMS carriers
-            promises.push(
-                egCore.pcrud.search('csc', 
-                    {active: 'true'}, 
-                    {'order_by':[
-                        {'class':'csc', 'field':'name'},
-                        {'class':'csc', 'field':'region'}
-                    ]}, {atomic : true}
-                ).then(function(carriers) {
-                    service.sms_carriers = carriers;
-                })
-            );
-        } else {
-            // if other promises are added below, this is not necessary.
-            promises.push($q.when());  
-        }
-
-        // other post-org-settings processing goes here,
-        // adding to promises as needed.
-
-        return $q.all(promises);
+        );
     };
 
     service.get_ident_types = function() {
-        if (egCore.env.cit) {
+        return egLovefield.reconstituteList('cit').then(function() {
             service.ident_types = egCore.env.cit.list;
             return $q.when();
-        } else {
-            return egCore.pcrud.retrieveAll('cit', {}, {atomic : true})
-            .then(function(types) { 
-                egCore.env.absorbList(types, 'cit')
-                service.ident_types = types 
-            });
-        }
+        });
     };
 
     service.get_net_access_levels = function() {
-        if (egCore.env.cnal) {
+        return egLovefield.reconstituteList('cnal').then(function() {
             service.net_access_levels = egCore.env.cnal.list;
             return $q.when();
-        } else {
-            return egCore.pcrud.retrieveAll('cnal', {}, {atomic : true})
-            .then(function(levels) { 
-                egCore.env.absorbList(levels, 'cnal')
-                service.net_access_levels = levels 
-            });
-        }
+        });
     }
 
     service.get_perm_groups = function() {
@@ -421,32 +895,23 @@ angular.module('egCoreMod')
             service.profiles = egCore.env.pgt.list;
             return service.set_edit_profiles();
         } else {
-            return egCore.pcrud.search('pgt', {parent : null}, 
-                {flesh : -1, flesh_fields : {pgt : ['children']}}
-            ).then(
-                function(tree) {
-                    egCore.env.absorbTree(tree, 'pgt')
-                    service.profiles = egCore.env.pgt.list;
-                    return service.set_edit_profiles();
-                }
-            );
+            return egLovefield.reconstituteTree('pgt').then(function(offline) {
+                service.profiles = egCore.env.pgt.list;
+                return service.set_edit_profiles();
+            });
         }
     }
 
     service.get_field_doc = function() {
-        return egCore.pcrud.search('fdoc', {
-            fm_class: ['au', 'ac', 'aua', 'actsc', 'asv', 'asvq', 'asva']})
-        .then(null, null, function(doc) {
-            if (!service.field_doc[doc.fm_class()]) {
-                service.field_doc[doc.fm_class()] = {};
-            }
-            service.field_doc[doc.fm_class()][doc.field()] = doc;
+        return egLovefield.getListFromOfflineCache('fdoc').then(function (list) {
+            angular.forEach(list, function(doc) {
+                service.field_doc[doc.fm_class()][doc.field()] = doc;
+            });
+            return $q.when();
         });
     };
 
     service.get_user_settings = function() {
-        var org_ids = egCore.org.ancestors(egCore.auth.user().ws_ou(), true);
-
         var static_types = [
             'circ.holds_behind_desk', 
             'circ.collections.exempt', 
@@ -456,135 +921,40 @@ angular.module('egCoreMod')
             'opac.default_sms_carrier', 
             'opac.default_sms_notify'];
 
-        return egCore.pcrud.search('cust', {
-            '-or' : [
-                {name : static_types}, // common user settings
-                {name : { // opt-in notification user settings
-                    'in': {
-                        select : {atevdef : ['opt_in_setting']}, 
-                        from : 'atevdef',
-                        // we only care about opt-in settings for 
-                        // event_defs our users encounter
-                        where : {'+atevdef' : {owner : org_ids}}
-                    }
-                }}
-            ]
-        }, {}, {atomic : true}).then(function(setting_types) {
+        angular.forEach(static_types, function (t) {
+            service.user_settings[t] = null;
+        });
 
-            angular.forEach(setting_types, function(stype) {
+        return egLovefield.getListFromOfflineCache('cust').then(function (list) {
+            angular.forEach(list, function(stype) {
                 service.user_setting_types[stype.name()] = stype;
                 if (static_types.indexOf(stype.name()) == -1) {
                     service.opt_in_setting_types[stype.name()] = stype;
                 }
+                if (stype.reg_default() != undefined) {
+                    service.user_settings[setting.name()] = 
+                        setting.reg_default();
+                }
             });
-
-            if (service.patron_id) {
-                // retrieve applied values for the current user 
-                // for the setting types we care about.
-
-                var setting_names = 
-                    setting_types.map(function(obj) { return obj.name() });
-
-                return egCore.net.request(
-                    'open-ils.actor', 
-                    'open-ils.actor.patron.settings.retrieve.authoritative',
-                    egCore.auth.token(),
-                    service.patron_id,
-                    setting_names
-                ).then(function(settings) {
-                    service.user_settings = settings;
-                });
-            } else {
-
-                // apply default user setting values
-                angular.forEach(setting_types, function(stype, index) {
-                    if (stype.reg_default() != undefined) {
-                        service.user_settings[setting.name()] = 
-                            setting.reg_default();
-                    }
-                });
-            }
+            return $q.when();
         });
     }
 
     service.invalidate_field = function(patron, field) {
-        console.log('Invalidating patron field ' + field);
-
-        return egCore.net.request(
-            'open-ils.actor',
-            'open-ils.actor.invalidate.' + field,
-            egCore.auth.token(), patron.id, null, patron.home_ou.id()
-
-        ).then(function(res) {
-            // clear the invalid value from the form
-            patron[field] = '';
-
-            // update last_xact_id so future save operations
-            // on this patron will be allowed
-            patron.last_xact_id = res.payload.last_xact_id[patron.id];
-        });
+        return;
     }
 
     service.dupe_patron_search = function(patron, type, value) {
-        var search;
-
-        console.log('Dupe search called with "'+ type +'" and value '+ value);
-
-        switch (type) {
-
-            case 'name':
-                var fname = patron.first_given_name;   
-                var lname = patron.family_name;   
-                if (!(fname && lname)) return $q.when({count:0});
-                search = {
-                    first_given_name : {value : fname, group : 0},
-                    family_name : {value : lname, group : 0}
-                };
-                break;
-
-            case 'email':
-                search = {email : {value : value, group : 0}};
-                break;
-
-            case 'ident':
-                search = {ident : {value : value, group : 2}};
-                break;
-
-            case 'phone':
-                search = {phone : {value : value, group : 2}};
-                break;
-
-            case 'address':
-                search = {};
-                angular.forEach(['street1', 'street2', 'city', 'post_code'],
-                    function(field) {
-                        if(value[field])
-                            search[field] = {value : value[field], group: 1};
-                    }
-                );
-                break;
-        }
-
-        return egCore.net.request( 
-            'open-ils.actor', 
-            'open-ils.actor.patron.search.advanced',
-            egCore.auth.token(), search, null, null, 1
-        ).then(function(res) {
-            res = res.filter(function(id) {return id != patron.id});
-            return {
-                count : res.length,
-                search : search
-            };
-        });
+        return $q.when({ search : search, count : 0 });
     }
 
     service.init_patron = function(current) {
 
         if (!current)
-            return $q.when(service.init_new_patron());
+            return service.init_new_patron();
 
         service.patron = current;
-        return $q.when(service.init_existing_patron(current));
+        return service.init_existing_patron(current)
     }
 
     service.ingest_address = function(patron, addr) {
@@ -655,7 +1025,6 @@ angular.module('egCoreMod')
             service.stat_cat_entry_maps[map.stat_cat.id] = map.stat_cat_entry;
         });
 
-        service.patron = patron;
         return patron;
     }
 
@@ -678,12 +1047,14 @@ angular.module('egCoreMod')
             _primary : 'on'
         };
 
+        var home_ou = egCore.org.get(service.org);
+
         var user = {
             isnew : true,
             active : true,
             card : card,
             cards : [card],
-            home_ou : egCore.org.get(egCore.auth.user().ws_ou()),
+            home_ou : home_ou,
             stat_cat_entries : [],
             groups : [],
             addresses : [addr]
@@ -1001,10 +1372,14 @@ angular.module('egCoreMod')
 
         if (!patron.isnew()) patron.ischanged(true);
 
-        return egCore.net.request(
-            'open-ils.actor', 
-            'open-ils.actor.patron.update',
-            egCore.auth.token(), patron);
+        return egLovefield.addOfflineXact({
+            user        : egCore.idl.toHash(patron),
+            timestamp   : parseInt(new Date().getTime() / 1000),
+            type        : 'register',
+            delta       : 0
+        }).then(function (success) {
+            if (success) return patron;
+        });
     }
 
     service.remove_staged_user = function() {
@@ -1018,33 +1393,7 @@ angular.module('egCoreMod')
     }
 
     service.save_user_settings = function(new_user, user_settings) {
-        // user_settings contains the values from the scope/form.
-        // service.user_settings contain the values from page load time.
-
-        var settings = {};
-        if (service.patron_id) {
-            // only update modified settings for existing patrons
-            angular.forEach(user_settings, function(val, key) {
-                if (val !== service.user_settings[key])
-                    settings[key] = val;
-            });
-
-        } else {
-            // all non-null setting values are updated for new patrons
-            angular.forEach(user_settings, function(val, key) {
-                if (val !== null) settings[key] = val;
-            });
-        }
-
-        if (Object.keys(settings).length == 0) return $q.when();
-
-        return egCore.net.request(
-            'open-ils.actor',
-            'open-ils.actor.patron.settings.update',
-            egCore.auth.token(), new_user.id(), settings
-        ).then(function(resp) {
-            return resp;
-        });
+        return;
     }
 
     // Applies field-specific validation regex's from org settings 
@@ -1092,13 +1441,16 @@ angular.module('egCoreMod')
 .controller('PatronRegCtrl',
        ['$scope','$routeParams','$q','$uibModal','$window','egCore',
         'patronSvc','patronRegSvc','egUnloadPrompt','egAlertDialog',
-        'egWorkLog',
+        'egWorkLog','$timeout','egLovefield','$rootScope',
 function($scope , $routeParams , $q , $uibModal , $window , egCore ,
          patronSvc , patronRegSvc , egUnloadPrompt, egAlertDialog ,
-         egWorkLog) {
+         egWorkLog , $timeout , egLovefield , $rootScope) {
+
+    $scope.rs = $rootScope;
+    if ($scope.workstation_obj) patronRegSvc.org = $scope.workstation_obj.owning_lib;
+    $scope.offline = true;
 
     $scope.page_data_loaded = false;
-    $scope.hold_notify_type = { phone : null, email : null, sms : null };
     $scope.clone_id = patronRegSvc.clone_id = $routeParams.clone_id;
     $scope.stage_username = 
         patronRegSvc.stage_username = $routeParams.stage_username;
@@ -1115,15 +1467,10 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
     // has at the currently selected patron home org unit.
     $scope.perms = {};
 
-    if (!$scope.edit_passthru) {
-        // in edit more, scope.edit_passthru is delivered to us by
-        // the enclosing controller.  In register mode, there is 
-        // no enclosing controller, so we create our own.
-        $scope.edit_passthru = {};
-    }
+    $scope.edit_passthru = {};
 
     // 0=all, 1=suggested, 2=all
-    $scope.edit_passthru.vis_level = 0; 
+    $scope.edit_passthru.vis_level = 2; 
 
     // Apply default values for new patrons during initial registration
     // prs is shorthand for patronSvc
@@ -1132,9 +1479,8 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
             // passsword may originate from staged user.
             $scope.generate_password();
         }
-        $scope.hold_notify_type.phone = true;
-        $scope.hold_notify_type.email = true;
-	$scope.hold_notify_type.sms = false;
+        $scope.hold_notify_phone = true;
+        $scope.hold_notify_email = true;
 
         // staged users may be loaded w/ a profile.
         $scope.set_expire_date();
@@ -1173,20 +1519,13 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         return field_patterns[cls][field];
     }
 
-    // Main page load function.  Kicks off tab init and data loading.
-    $q.all([
-
-        $scope.initTab ? // initTab comes from patron app
-            $scope.initTab('edit', $routeParams.id) : $q.when(),
-
-        patronRegSvc.init(),
-
-    ]).then(function(){ return patronRegSvc.init_patron(patronSvc ? patronSvc.current : patronRegSvc.patron ) })
-      .then(function(patron) {
+    patronRegSvc.offlineMode($scope.offline); // force offline if ng-init'd to do so
+    patronRegSvc.init().then(function() {
         // called after initTab and patronRegSvc.init have completed
+    
+        var prs = patronRegSvc; // brevity
         // in standalone mode, we have no patronSvc
-        var prs = patronRegSvc;
-        $scope.patron = patron;
+        $scope.patron = prs.init_patron(patronSvc ? patronSvc.current : null);
         $scope.field_doc = prs.field_doc;
         $scope.edit_profiles = prs.edit_profiles;
         $scope.ident_types = prs.ident_types;
@@ -1201,30 +1540,30 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         $scope.stat_cat_entry_maps = prs.stat_cat_entry_maps;
         $scope.stage_user = prs.stage_user;
         $scope.stage_user_requestor = prs.stage_user_requestor;
-
+    
         $scope.user_settings = prs.user_settings;
+        // clone the user settings back into the patronRegSvc so
+        // we have a copy of the original state of the settings.
         prs.user_settings = {};
-
+        angular.forEach($scope.user_settings, function(val, key) {
+            prs.user_settings[key] = val;
+        });
+    
         extract_hold_notify();
-        if ($scope.patron.isnew)
-            set_new_patron_defaults(prs);
-
         $scope.handle_home_org_changed();
-
+    
         if ($scope.org_settings['ui.patron.edit.default_suggested'])
             $scope.edit_passthru.vis_level = 1;
-
-        // Stat cats are fetched from open-ils.storage, where 't'==1
-        $scope.hasRequiredStatCat = prs.stat_cats.filter(
-                function(cat) {return cat.required() == 1} ).length > 0;
-
+    
+        if ($scope.patron.isnew) 
+            set_new_patron_defaults(prs);
+    
         $scope.page_data_loaded = true;
-
+    
         prs.set_field_patterns(field_patterns);
         apply_username_regex();
     });
 
-
     // update the currently displayed field documentation
     $scope.set_selected_field_doc = function(cls, field) {
         $scope.selected_field_doc = $scope.field_doc[cls][field];
@@ -1320,7 +1659,7 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         if (field == 'passwd' && $scope.patron && !$scope.patron.isnew) 
           return false;
 
-        return (field_visibility[cls + '.' + field] == 3);
+        return (field_visibility[cls + '.' + field] == 3 || default_field_visibility[cls + '.' + field] == 3);
     }
 
     // generates a random 4-digit password
@@ -1391,6 +1730,7 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
     } 
 
     $scope.post_code_changed = function(addr) { 
+        if ($scope.offline) return;
         egCore.net.request(
             'open-ils.search', 'open-ils.search.zip', addr.post_code)
         .then(function(resp) {
@@ -1425,21 +1765,8 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
 
     $scope.barcode_changed = function(bc) {
         if (!bc) return;
-        $scope.dupe_barcode = false;
-        egCore.net.request(
-            'open-ils.actor',
-            'open-ils.actor.barcode.exists',
-            egCore.auth.token(), bc
-        ).then(function(resp) {
-            if (resp == '1') { // duplicate card
-                $scope.dupe_barcode = true;
-                console.log('duplicate barcode detected: ' + bc);
-            } else {
-                if (!$scope.patron.usrname)
-                    $scope.patron.usrname = bc;
-                // No dupe -- A-OK
-            }
-        });
+        if (!$scope.patron.usrname)
+            $scope.patron.usrname = bc;
     }
 
     $scope.cards_dialog = function() {
@@ -1500,18 +1827,21 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
     // Translate hold notify preferences from the form/scope back into a 
     // single user setting value for opac.hold_notify.
     function compress_hold_notify() {
-        var hold_notify_methods = [];
-        if ($scope.hold_notify_type.phone) {
-            hold_notify_methods.push('phone');
+        var hold_notify = '';
+        var splitter = '';
+        if ($scope.hold_notify_phone) {
+            hold_notify = 'phone';
+            splitter = ':';
         }
-        if ($scope.hold_notify_type.email) {
-            hold_notify_methods.push('email');
+        if ($scope.hold_notify_email) {
+            hold_notify = splitter + 'email';
+            splitter = ':';
         }
-        if ($scope.hold_notify_type.sms) {
-            hold_notify_methods.push('sms');
+        if ($scope.hold_notify_sms) {
+            hold_notify = splitter + 'sms';
+            splitter = ':';
         }
-
-        $scope.user_settings['opac.hold_notify'] = hold_notify_methods.join(':');
+        $scope.user_settings['opac.hold_notify'] = hold_notify;
     }
 
     // dialog for selecting additional permission groups
@@ -1576,11 +1906,11 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
     }
 
     function extract_hold_notify() {
-        var notify = $scope.user_settings['opac.hold_notify'];
+        notify = $scope.user_settings['opac.hold_notify'];
         if (!notify) return;
-        $scope.hold_notify_type.phone = Boolean(notify.match(/phone/));
-        $scope.hold_notify_type.email = Boolean(notify.match(/email/));
-        $scope.hold_notify_type.sms = Boolean(notify.match(/sms/));
+        $scope.hold_notify_phone = Boolean(notify.match(/phone/));
+        $scope.hold_notify_email = Boolean(notify.match(/email/));
+        $scope.hold_notify_sms = Boolean(notify.match(/sms/));
     }
 
     $scope.invalidate_field = function(field) {
@@ -1588,6 +1918,7 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
     }
 
     address_alert = function(addr) {
+        if ($scope.offline) return;
         var args = {
             street1: addr.street1,
             street2: addr.street2,
@@ -1623,12 +1954,8 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         });
     }
 
-    $scope.handle_home_org_changed = function() {
-        org_id = $scope.patron.home_ou.id();
-        patronRegSvc.has_perms_for_org(org_id).then(function(map) {
-            angular.forEach(map, function(v, k) { $scope.perms[k] = v });
-        });
-    }
+    // Dummy function in offline mode
+    $scope.handle_home_org_changed = function() {}
 
     // This is called with every character typed in a form field,
     // since that's the only way to gaurantee something has changed.
@@ -1638,13 +1965,13 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         // it's been called before.  This will allow for re-attach after
         // the user clicks through the unload warning. egUnloadPrompt
         // will ensure we only attach once.
-        egUnloadPrompt.attach($scope);
+        egUnloadPrompt.attach($rootScope);
     }
 
     // also monitor when form is changed *by the user*, as using
     // an ng-change handler doesn't work with eg-date-input
     $scope.$watch('reg_form.$pristine', function(newVal, oldVal) {
-        if (!newVal) egUnloadPrompt.attach($scope);
+        if (!newVal) egUnloadPrompt.attach($rootScope);
     });
 
     // username regex (if present) must be removed any time
@@ -1674,10 +2001,13 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
     // The alternative is ng-change, but it's called with each character
     // typed, which would be overkill for many of the actions called here.
     $scope.handle_field_changed = function(obj, field_name) {
+        if (!obj) return;
+
         var cls = obj.classname; // set by egIdl
         var value = obj[field_name];
 
-        console.log('changing field ' + field_name + ' to ' + value);
+        // Hush!
+        //console.log('changing field ' + field_name + ' to ' + value);
 
         switch (field_name) {
             case 'day_phone' : 
@@ -1686,51 +2016,19 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
                     $scope.org_settings['patron.password.use_phone']) {
                     $scope.patron.passwd = phone.substr(-4);
                 }
-            case 'evening_phone' : 
-            case 'other_phone' : 
-                $scope.dupe_value_changed('phone', value);
-                break;
-
-            case 'ident_value':
-            case 'ident_value2':
-                $scope.dupe_value_changed('ident', value);
-                break;
-
-            case 'first_given_name':
-            case 'family_name':
-                $scope.dupe_value_changed('name', value);
-                break;
-
-            case 'email':
-                $scope.dupe_value_changed('email', value);
-                break;
-
-            case 'street1':
-            case 'street2':
-            case 'city':
-                // dupe search on address wants the address object as the value.
-                $scope.dupe_value_changed('address', obj);
-                address_alert(obj);
-                break;
-
-            case 'post_code':
-                $scope.post_code_changed(obj);
-                break;
-
-            case 'usrname':
-                patronRegSvc.check_dupe_username(value)
-                .then(function(yes) {$scope.dupe_username = Boolean(yes)});
                 break;
 
             case 'barcode':
-                // TODO: finish barcode_changed handler.
-                $scope.barcode_changed(value);
                 apply_username_regex();
+                $scope.barcode_changed(value);
                 break;
 
             case 'dob':
                 maintain_juvenile_flag();
                 break;
+
+            default:
+                break;
         }
     }
 
@@ -1763,9 +2061,7 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
 
     // Returns true if the Save and Save & Clone buttons should be disabled.
     $scope.edit_passthru.hide_save_actions = function() {
-        return $scope.patron.isnew ?
-            !$scope.perms.CREATE_USER : 
-            !$scope.perms.UPDATE_USER;
+        return false;
     }
 
     // Returns true if any input elements are tagged as invalid
@@ -1804,73 +2100,10 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         var updated_user;
 
         patronRegSvc.save_user($scope.patron)
+        .then($scope.rs.save_offline_xacts)
         .then(function(new_user) { 
-            if (new_user && new_user.classname) {
-                updated_user = new_user;
-                return patronRegSvc.save_user_settings(
-                    new_user, $scope.user_settings); 
-            } else {
-                var evt = egCore.evt.parse(new_user);
-
-                if (evt && evt.textcode == 'XACT_COLLISION') {
-                    return egAlertDialog.open(
-                        egCore.strings.PATRON_EDIT_COLLISION).result;
-                }
-
-                // debug only -- should not get here.
-                alert('Patron update failed. \n\n' + js2JSON(new_user));
-            }
-
-        }).then(function() {
-
-            // only remove the staged user if the update succeeded.
-            if (updated_user) 
-                return patronRegSvc.remove_staged_user();
-
-            return $q.when();
-
-        }).then(function() {
-
-            // linked groups for new users must be created after the new
-            // user is created.
-            if ($scope.patron.isnew && 
-                $scope.patron.groups && $scope.patron.groups.length) {
-                var ids = $scope.patron.groups.map(function(g) {return g.id()});
-                return patronRegSvc.apply_secondary_groups(updated_user.id(), ids)
-            }
-
-            return $q.when();
-
-        }).then(function() {
-
-            if (updated_user) {
-                egWorkLog.record(
-                    $scope.patron.isnew
-                    ? egCore.strings.EG_WORK_LOG_REGISTERED_PATRON
-                    : egCore.strings.EG_WORK_LOG_EDITED_PATRON, {
-                        'action' : $scope.patron.isnew ? 'registered_patron' : 'edited_patron',
-                        'patron_id' : updated_user.id()
-                    }
-                );
-            }
-
-            // reloading the page means potentially losing some information
-            // (e.g. last patron search), but is the only way to ensure all
-            // components are properly updated to reflect the modified patron.
-            if (updated_user && save_args.clone) {
-                // open a separate tab for registering a new 
-                // patron from our cloned data.
-                var url = 'https://' 
-                    + $window.location.hostname 
-                    + egCore.env.basePath 
-                    + '/circ/patron/register/clone/' 
-                    + updated_user.id();
-                $window.open(url, '_blank').focus();
-
-            } else {
-                // reload the current page
-                $window.location.href = location.href;
-            }
+            // reload the current page
+            $window.location.href = location.href;
         });
     }
 }])
diff --git a/Open-ILS/web/js/ui/default/staff/package.json b/Open-ILS/web/js/ui/default/staff/package.json
index cd3a9bb..d972491 100644
--- a/Open-ILS/web/js/ui/default/staff/package.json
+++ b/Open-ILS/web/js/ui/default/staff/package.json
@@ -15,6 +15,7 @@
     "angular-mocks": "~1.5.0",
     "angular-route": "~1.5.0",
     "angular-tree-control": "~0.2.28",
+    "angular-tablesort": "^1.4.1",
     "angular-order-object-by": "rxfork/ngOrderObjectBy#npm",
     "lovefield": "*",
     "moment": "*",
diff --git a/Open-ILS/web/js/ui/default/staff/services/auth.js b/Open-ILS/web/js/ui/default/staff/services/auth.js
index 4e98956..3ee2296 100644
--- a/Open-ILS/web/js/ui/default/staff/services/auth.js
+++ b/Open-ILS/web/js/ui/default/staff/services/auth.js
@@ -61,23 +61,33 @@ function($q , $timeout , $rootScope , $window , $location , egNet , egHatch) {
 
         if (token) {
 
-            egNet.request(
-                'open-ils.auth',
-                'open-ils.auth.session.retrieve', token)
-
-            .then(function(user) {
-                if (user && user.classname) {
-                    // authtoken test succeeded
-                    service.user(user);
-                    service.poll();
-                    service.check_workstation(deferred);
-
-                } else {
-                    // authtoken test failed
-                    egHatch.clearLoginSessionItems();
-                    deferred.reject(); 
-                }
-            });
+            if (lf.isOffline && !$location.path().match(/\/session/) ) {
+                // Just stop here if we're in the offline interface but not on the session tab
+                $timeout(function(){deferred.resolve()});
+            } else if (lf.isOffline && $location.path().match(/\/session/) && !$window.navigator.onLine) {
+                // Likewise, if we're in the offline interface on the session tab and the network is down.
+                // The session tab itself will redirect appropriately due to no network.
+                $timeout(function(){deferred.resolve()});
+            } else {
+                // Otherwise, check the token.  This will freeze all other interfaces, which is what we want.
+                egNet.request(
+                    'open-ils.auth',
+                    'open-ils.auth.session.retrieve', token)
+    
+                .then(function(user) {
+                    if (user && user.classname) {
+                        // authtoken test succeeded
+                        service.user(user);
+                        service.poll();
+                        service.check_workstation(deferred);
+    
+                    } else {
+                        // authtoken test failed
+                        egHatch.clearLoginSessionItems();
+                        deferred.reject(); 
+                    }
+                });
+            }
 
         } else {
             // no authtoken to test
diff --git a/Open-ILS/web/js/ui/default/staff/services/env.js b/Open-ILS/web/js/ui/default/staff/services/env.js
index ad41fc1..4fe755a 100644
--- a/Open-ILS/web/js/ui/default/staff/services/env.js
+++ b/Open-ILS/web/js/ui/default/staff/services/env.js
@@ -46,12 +46,20 @@ angular.module('egCoreMod')
 
 // env fetcher
 .factory('egEnv', 
-       ['$q','$window','egAuth','egPCRUD','egIDL',
-function($q,  $window , egAuth,  egPCRUD,  egIDL) { 
+       ['$q','$window','$injector','egAuth','egPCRUD','egIDL',
+function($q,  $window , $injector , egAuth,  egPCRUD,  egIDL) { 
 
     var service = {
         // collection of custom loader functions
-        loaders : []
+        loaders : [],
+
+        // Add class hints to this list when offline does not need them and
+        // if they cause "Maximum call stack size exceeded" console errors.
+        // If offline does need a list that causes problems, a custom loader
+        // will be necessary.
+        // We'll start with authority-related classes causing problems in the
+        // staff catalog.
+        ignoreOffline : ['at','acs','abaafm','aba','acsbf','acsaf']
     };
 
 
@@ -87,19 +95,25 @@ function($q,  $window , egAuth,  egPCRUD,  egIDL) {
     /** given a tree-shaped collection, captures the tree and
      *  flattens the tree for absorption.
      */
-    service.absorbTree = function(tree, class_) {
+    service.absorbTree = function(tree, class_, noOffline) {
+        if (service[class_] && service[class_].loaded) return;
+
         var list = [];
         function squash(node) {
             list.push(node);
             angular.forEach(node.children(), squash);
         }
         squash(tree);
-        var blob = service.absorbList(list, class_);
+        var blob = service.absorbList(list, class_, noOffline);
         blob.tree = tree;
     };
 
+    var egLovefield; // we'll inject it manually
+
     /** caches the object list both as the list and an id => object map */
-    service.absorbList = function(list, class_) {
+    service.absorbList = function(list, class_, noOffline) {
+        if (service[class_] && service[class_].loaded) return service[class_];
+
         var blob;
         var pkey = egIDL.classes[class_].pkey;
 
@@ -116,8 +130,19 @@ function($q,  $window , egAuth,  egPCRUD,  egIDL) {
             blob = {list : list, map : {}};
         }
 
+        if (!noOffline && service.ignoreOffline.indexOf(class_) < 0) {
+            if (!egLovefield) {
+                egLovefield = $injector.get('egLovefield');
+            }
+            console.debug('About to cache a list of ' + class_ + ' objects...');
+            egLovefield.isCacheGood(class_).then(function(good) {
+                if (!good) egLovefield.setListInOfflineCache(class_, blob.list);
+            });
+        }
+
         angular.forEach(list, function(item) {blob.map[item[pkey]()] = item});
         service[class_] = blob;
+        service[class_].loaded = true;
         return blob;
     };
 
@@ -135,35 +160,45 @@ function($q,  $window , egAuth,  egPCRUD,  egIDL) {
     service.classLoaders = {
         aou : function() {
 
-            // EXPERIMENT: cache the org tree in session storage.
-            // This means that if the org tree changes, users will have to
-            // open the client in a new browser tab to clear the cached tree.
-            var treeJSON = $window.sessionStorage.getItem('eg.env.aou.tree');
-            if (treeJSON) {
-                console.debug('serving org tree from cache');
-                var tree = JSON2js(treeJSON);
-                service.absorbTree(tree, 'aou')
-                return $q.when(tree);
-            }
-
-            // sort orgs at each level by shortname
-            function sort_aou(node) {
-                node.children(node.children().sort(function(a, b) {
-                    return a.shortname() < b.shortname() ? -1 : 1;
-                }));
-                angular.forEach(node.children(), sort_aou);
+            if (!egLovefield) {
+                egLovefield = $injector.get('egLovefield');
             }
 
-            return egPCRUD.search('aou', {parent_ou : null}, 
-                {flesh : -1, flesh_fields : {aou : ['children', 'ou_type']}}
-            ).then(
-                function(tree) {
-                    sort_aou(tree);
-                    $window.sessionStorage.setItem(
-                        'eg.env.aou.tree', js2JSON(tree));
+            return egLovefield.reconstituteTree('aou').then(function(offline) {
+                if (offline) return $q.when();
+                if (service.aou && service.aou.loaded) return $q.when();
+    
+                // EXPERIMENT: cache the org tree in session storage.
+                // This means that if the org tree changes, users will have to
+                // open the client in a new browser tab to clear the cached tree.
+                var treeJSON = $window.sessionStorage.getItem('eg.env.aou.tree');
+                if (treeJSON) {
+                    console.debug('serving org tree from cache');
+                    var tree = JSON2js(treeJSON);
                     service.absorbTree(tree, 'aou')
+                    return $q.when(tree);
                 }
-            );
+    
+                // sort orgs at each level by shortname
+                function sort_aou(node) {
+                    node.children(node.children().sort(function(a, b) {
+                        return a.shortname() < b.shortname() ? -1 : 1;
+                    }));
+                    angular.forEach(node.children(), sort_aou);
+                }
+    
+                return egPCRUD.search('aou', {parent_ou : null}, 
+                    {flesh : -1, flesh_fields : {aou : ['children', 'ou_type']}}
+                ).then(
+                    function(tree) {
+                        sort_aou(tree);
+                        $window.sessionStorage.setItem(
+                            'eg.env.aou.tree', js2JSON(tree));
+                        service.absorbTree(tree, 'aou');
+                        return $q.when();
+                    }
+                );
+            });
         },
     };
 
diff --git a/Open-ILS/web/js/ui/default/staff/services/file.js b/Open-ILS/web/js/ui/default/staff/services/file.js
index 83e0d29..829d4ca 100644
--- a/Open-ILS/web/js/ui/default/staff/services/file.js
+++ b/Open-ILS/web/js/ui/default/staff/services/file.js
@@ -35,18 +35,72 @@ angular.module('egCoreMod')
             defaultFileName: '='
         },
         link: function (scope, element, attributes) {
+            var name = scope.defaultFileName || 'evergreen-json-export';
             element.bind('click', function (clickEvent) {
                 if (scope.generator) {
                     scope.generator().then(function(value) {
                         var data = new Blob([JSON.stringify(value)], {type : 'application/json'});
-                        FileSaver.saveAs(data, scope.defaultFileName);
+                        FileSaver.saveAs(data, name);
                     });
                 } else {
                     var data = new Blob([JSON.stringify(scope.container)], {type : 'application/json'});
-                    FileSaver.saveAs(data, scope.defaultFileName);
+                    FileSaver.saveAs(data, name);
                 }
             });
         }
     }
 }])
+
+// The following directives use a attr instead of binding to get the default file name!
+.directive('egStringExporter', ['FileSaver', 'Blob', function(FileSaver, Blob) {
+    return {
+        scope: {
+            contentType: '=',
+            string: '=',
+            generator: '=',
+            defaultFileName: '@'
+        },
+        link: function (scope, element, attributes) {
+            var type = scope.contentType || 'text/plain';
+            var name = scope.defaultFileName || 'evergreen-string-export';
+            element.bind('click', function (clickEvent) {
+                if (scope.generator) {
+                    scope.generator().then(function(value) {
+                        var data = new Blob([value], {type : type});
+                        FileSaver.saveAs(data, name);
+                    });
+                } else {
+                    var data = new Blob([scope.string], {type : type});
+                    FileSaver.saveAs(data, name);
+                }
+            });
+        }
+    }
+}])
+
+.directive('egLineExporter', ['FileSaver', 'Blob', function(FileSaver, Blob) {
+    return {
+        scope: {
+            contentType: '=',
+            jsonArray: '=',
+            defaultFileName: '@'
+        },
+        link: function (scope, element, attributes) {
+            element.bind('click', function (clickEvent) {
+                var type = scope.contentType || 'text/plain';
+                var fname = scope.defaultFileName || 'evergreen-string-export';
+                FileSaver.saveAs(
+                    new Blob(
+                        scope.jsonArray.map(function (line) {
+                            return JSON.stringify(line) + '\n';
+                        }),
+                        {type : type}
+                    ),
+                    fname
+                );
+            });
+        }
+    }
+}])
+
 ;
diff --git a/Open-ILS/web/js/ui/default/staff/services/hatch.js b/Open-ILS/web/js/ui/default/staff/services/hatch.js
index 4fee7cb..0a27b15 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -310,6 +310,42 @@ angular.module('egCoreMod')
         $window.localStorage.setItem(key, jsonified);
     }
 
+    service.appendItem = function(key, value) {
+        if (!service.useSettings())
+            return $q.when(service.appendLocalItem(key, value));
+
+        if (service.hatchAvailable)
+            return service.appendRemoteItem(key, value);
+
+        if (service.keyIsOnCall(key)) {
+            console.warn("Unable to appendItem in Hatch: " + 
+                key + ". Setting in local storage instead");
+
+            return $q.when(service.appendLocalItem(key, value));
+        }
+
+        console.error("Unable to appendItem in Hatch: " + key);
+        return $q.reject();
+    }
+
+    // append the value to a stored or new item
+    service.appendRemoteItem = function(key, value) {
+        service.keyCache[key] = value;
+        return service.attemptHatchDelivery({
+            key : key, 
+            content : value, 
+            action : 'append',
+        });
+    }
+
+    service.appendLocalItem = function(key, value, jsonified) {
+        if (jsonified === undefined ) 
+            jsonified = JSON.stringify(value);
+
+        var old_value = $window.localStorage.getItem(key) || '';
+        $window.localStorage.setItem( key, old_value + jsonified );
+    }
+
     // Set the value for the given key.  
     // "LoginSession" items are removed when the user logs out or the 
     // browser is closed.
diff --git a/Open-ILS/web/js/ui/default/staff/services/idl.js b/Open-ILS/web/js/ui/default/staff/services/idl.js
index 3e41715..1b47ec1 100644
--- a/Open-ILS/web/js/ui/default/staff/services/idl.js
+++ b/Open-ILS/web/js/ui/default/staff/services/idl.js
@@ -50,8 +50,6 @@ angular.module('egCoreMod')
                             else
                                 obj.a[i][j] = angular.copy(thing[j]);
                         }
-                    } else {
-                        obj.a[i] = angular.copy(thing);
                     }
                 }
             }
diff --git a/Open-ILS/web/js/ui/default/staff/services/lovefield.js b/Open-ILS/web/js/ui/default/staff/services/lovefield.js
new file mode 100644
index 0000000..a05e824
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/services/lovefield.js
@@ -0,0 +1,406 @@
+var osb = lf.schema.create('offline', 2);
+
+osb.createTable('Object').
+    addColumn('type', lf.Type.STRING).          // class hint
+    addColumn('id', lf.Type.STRING).           // obj id
+    addColumn('object', lf.Type.OBJECT).
+    addPrimaryKey(['type','id']);
+
+osb.createTable('CacheDate').
+    addColumn('type', lf.Type.STRING).          // class hint
+    addColumn('cachedate', lf.Type.DATE_TIME).  // when was it last updated
+    addPrimaryKey(['type']);
+
+osb.createTable('Setting').
+    addColumn('name', lf.Type.STRING).
+    addColumn('value', lf.Type.STRING).
+    addPrimaryKey(['name']);
+
+osb.createTable('StatCat').
+    addColumn('id', lf.Type.INTEGER).
+    addColumn('value', lf.Type.OBJECT).
+    addPrimaryKey(['id']);
+
+osb.createTable('OfflineXact').
+    addColumn('seq', lf.Type.INTEGER).
+    addColumn('value', lf.Type.OBJECT).
+    addPrimaryKey(['seq'], true);
+
+osb.createTable('OfflineBlocks').
+    addColumn('barcode', lf.Type.STRING).
+    addColumn('reason', lf.Type.STRING).
+    addPrimaryKey(['barcode']);
+
+lf.connecting = true;
+osb.connect().then(function (db) {
+    lf.offlineDB = db;
+    lf.connecting = false;
+});
+
+/**
+ * Core Service - egLovefield
+ *
+ * Lovefield wrapper factory for low level offline stuff
+ *
+ */
+angular.module('egCoreMod')
+
+.factory('egLovefield', ['$q','$rootScope','egCore','$timeout', 
+                 function($q , $rootScope , egCore , $timeout) { 
+    
+    var service = {};
+
+    function connectOrGo (resolver) {
+        if (lf.offlineDB) {
+            return resolver();
+        }
+
+        // apparently, this might take a while...
+        if (lf.connecting) return $timeout(function() {
+                return connectOrGo(resolver);
+        });
+
+        console.log('egLovefield connecting to offline DB');
+
+        try {
+            return osb.connect().then(function (db) {
+                lf.offlineDB = db;
+                return resolver();
+            });
+        } catch (err) {
+                alert('attempted reconnect failure: ' + err.toString());
+        }
+    }
+
+    service.isCacheGood = function (type) {
+
+        return connectOrGo(function() {
+            var cacheDate = lf.offlineDB.getSchema().table('CacheDate');
+
+            return lf.offlineDB.
+                select(cacheDate.cachedate).
+                from(cacheDate).
+                where(cacheDate.type.eq(type)).
+                exec().then(function(results) {
+                    if (results.length == 0) {
+                        return $q.when(false);
+                    }
+
+                    var now = new Date();
+    
+                    // hard-coded 1 day offline cache timeout
+                    return $q.when((now.getTime() - results[0]['cachedate'].getTime()) <= 86400000);
+                })
+        });
+    }
+
+    service.destroyPendingOfflineXacts = function () {
+        return connectOrGo(function() {
+            var table = lf.offlineDB.getSchema().table('OfflineXact');
+            return lf.offlineDB.
+                delete().
+                from(table).
+                exec();
+        });
+    }
+
+    service.havePendingOfflineXacts = function () {
+        return connectOrGo(function() {
+            var table = lf.offlineDB.getSchema().table('OfflineXact');
+            return lf.offlineDB.
+                select(table.reason).
+                from(table).
+                exec().
+                then(function(list) {
+                    return $q.when(Boolean(list.length > 0))
+                });
+        });
+    }
+
+    service.retrievePendingOfflineXacts = function () {
+        return connectOrGo(function() {
+            var table = lf.offlineDB.getSchema().table('OfflineXact');
+            return lf.offlineDB.
+                select(table.value).
+                from(table).
+                exec().
+                then(function(list) {
+                    return $q.when(list.map(function(x) { return x.value }))
+                });
+        });
+    }
+
+    service.destroyOfflineBlocks = function () {
+        return connectOrGo(function() {
+            var table = lf.offlineDB.getSchema().table('OfflineBlocks');
+            return $q.when(
+                lf.offlineDB.
+                    delete().
+                    from(table).
+                    exec()
+            );
+        });
+    }
+
+    service.addOfflineBlock = function (barcode, reason) {
+        return connectOrGo(function() {
+            var table = lf.offlineDB.getSchema().table('OfflineBlocks');
+            return $q.when(
+                lf.offlineDB.
+                    insertOrReplace().
+                    into(table).
+                    values([ table.createRow({ barcode : barcode, reason : reason }) ]).
+                    exec()
+            );
+        });
+    }
+
+    // Returns a promise with true for blocked, false for not blocked
+    service.testOfflineBlock = function (barcode) {
+        return connectOrGo(function() {
+            var table = lf.offlineDB.getSchema().table('OfflineBlocks');
+            return lf.offlineDB.
+                select(table.reason).
+                from(table).
+                where(table.barcode.eq(barcode)).
+                exec().then(function(list) {
+                    if(list.length > 0) return $q.when(list[0].reason);
+                    return $q.when(null);
+                });
+        });
+    }
+
+    service.addOfflineXact = function (obj) {
+        return connectOrGo(function() {
+            var table = lf.offlineDB.getSchema().table('OfflineXact');
+            return $q.when(
+                lf.offlineDB.
+                    insertOrReplace().
+                    into(table).
+                    values([ table.createRow({ value : obj }) ]).
+                    exec()
+            );
+        });
+    }
+
+    service.setStatCatsCache = function (statcats) {
+        if (lf.isOffline) return $q.when();
+
+        return connectOrGo(function() {
+            var table = lf.offlineDB.getSchema().table('StatCat');
+            var rlist = [];
+
+            angular.forEach(statcats, function (val) {
+                rlist.push(table.createRow({
+                    id    : val.id(),
+                    value : egCore.idl.toHash(val)
+                }));
+            });
+            return lf.offlineDB.
+                insertOrReplace().
+                into(table).
+                values(rlist).
+                exec();
+        });
+    }
+
+    service.getStatCatsCache = function () {
+        return connectOrGo(function() {
+
+            var table = lf.offlineDB.getSchema().table('StatCat');
+            var result = [];
+            return lf.offlineDB.
+                select(table.value).
+                from(table).
+                exec().then(function(list) {
+                    angular.forEach(list, function (s) {
+                        var sc = egCore.idl.fromHash('actsc', s.value);
+    
+                        if (angular.isArray(sc.default_entries())) {
+                            sc.default_entries(
+                                sc.default_entries().map( function (k) {
+                                    return egCore.idl.fromHash('actsced', k);
+                                })
+                            );
+                        }
+    
+                        if (angular.isArray(sc.entries())) {
+                            sc.entries(
+                                sc.entries().map( function (k) {
+                                    return egCore.idl.fromHash('actsce', k);
+                                })
+                            );
+                        }
+    
+                        result.push(sc);
+                    });
+                    return $q.when(result);
+                });
+    
+        });
+    }
+
+    service.setSettingsCache = function (settings) {
+        if (lf.isOffline) return $q.when();
+
+        return connectOrGo(function() {
+
+            var table = lf.offlineDB.getSchema().table('Setting');
+            var rlist = [];
+
+            angular.forEach(settings, function (val, key) {
+                rlist.push(
+                    table.createRow({
+                        name  : key,
+                        value : JSON.stringify(val)
+                    })
+                );
+            });
+
+            return lf.offlineDB.
+                insertOrReplace().
+                into(table).
+                values(rlist).
+                exec();
+        });
+    }
+
+    service.getSettingsCache = function (settings) {
+        return connectOrGo(function() {
+
+            var table = lf.offlineDB.getSchema().table('Setting');
+
+            var search_pred = table.name.isNotNull();
+            if (settings && settings.length) {
+                search_pred = table.name.in(settings);
+            }
+                
+            return lf.offlineDB.
+                select(table.name, table.value).
+                from(table).
+                where(search_pred).
+                exec().then(function(list) {
+                    angular.forEach(list, function (s) {
+                        s.value = JSON.parse(s.value)
+                    });
+                    return $q.when(list);
+                });
+        });
+    }
+
+    service.setListInOfflineCache = function (type, list) {
+        if (lf.isOffline) return $q.when();
+
+        return connectOrGo(function() {
+
+            service.isCacheGood(type).then(function(good) {
+                if (!good) {
+                    var object = lf.offlineDB.getSchema().table('Object');
+                    var cacheDate = lf.offlineDB.getSchema().table('CacheDate');
+                    var pkey = egCore.idl.classes[type].pkey;
+        
+                    angular.forEach(list, function(item) {
+                        var row = object.createRow({
+                            type    : type,
+                            id      : '' + item[pkey](),
+                            object  : egCore.idl.toHash(item)
+                        });
+                        lf.offlineDB.insertOrReplace().into(object).values([row]).exec();
+                    });
+        
+                    var row = cacheDate.createRow({
+                        type      : type,
+                        cachedate : new Date()
+                    });
+        
+                    console.log('egLovefield saving ' + type + ' list');
+                    lf.offlineDB.insertOrReplace().into(cacheDate).values([row]).exec();
+                }
+            })
+        });
+    }
+
+    service.getListFromOfflineCache = function(type) {
+        return connectOrGo(function() {
+
+            var object = lf.offlineDB.getSchema().table('Object');
+
+            return lf.offlineDB.
+                select(object.object).
+                from(object).
+                where(object.type.eq(type)).
+                exec().then(function(results) {
+                    return $q.when(results.map(function(item) {
+                        return egCore.idl.fromHash(type,item['object'])
+                    }));
+                });
+        });
+    }
+
+    service.reconstituteList = function(type) {
+        if (lf.isOffline) {
+            console.log('egLovefield reading ' + type + ' list');
+            return service.getListFromOfflineCache(type).then(function (list) {
+                egCore.env.absorbList(list, type, true)
+                return $q.when(true);
+            });
+        }
+        return $q.when(false);
+    }
+
+    service.reconstituteTree = function(type) {
+        if (lf.isOffline) {
+            console.log('egLovefield reading ' + type + ' tree');
+
+            var pkey = egCore.idl.classes[type].pkey;
+            var parent_field = 'parent';
+
+            if (type == 'aou') {
+                parent_field = 'parent_ou';
+            }
+
+            return service.getListFromOfflineCache(type).then(function (list) {
+                var hash = {};
+                var top = null;
+                angular.forEach(list, function (item) {
+
+                    // Special case for aou, to reconstitue ou_type
+                    if (type == 'aou') {
+                        if (item.ou_type()) {
+                            item.ou_type( egCore.idl.fromHash('aout', item.ou_type()) );
+                        }
+                    }
+
+                    hash[''+item[pkey]()] = item;
+                    if (!item[parent_field]()) {
+                        top = item;
+                    } else if (angular.isObject(item[parent_field]())) {
+                        // un-objectify the parent
+                        item[parent_field](
+                            item[parent_field]()[pkey]()
+                        );
+                    }
+                });
+
+                angular.forEach(list, function (item) {
+                    item.children([]); // just clear it out if there's junk in there
+
+                    if (item[parent_field]()) {
+                        item[parent_field]( hash[''+item[parent_field]()] );
+                    }
+
+                    item.children( list.filter(function (kid) {
+                        return kid[parent_field]() == item[pkey]();
+                    }) );
+                });
+
+                egCore.env.absorbTree(top, type, true)
+                return $q.when(true)
+            });
+        }
+        return $q.when(false);
+    }
+
+    return service;
+}]);
+
diff --git a/Open-ILS/web/js/ui/default/staff/services/navbar.js b/Open-ILS/web/js/ui/default/staff/services/navbar.js
index 713d9f3..41d5ed8 100644
--- a/Open-ILS/web/js/ui/default/staff/services/navbar.js
+++ b/Open-ILS/web/js/ui/default/staff/services/navbar.js
@@ -5,47 +5,26 @@ angular.module('egCoreMod')
         restrict : 'AE',
         transclude : true,
         templateUrl : 'eg-navbar-template',
-        link : function(scope, element, attrs) {
+        controller:['$scope','$window','$location','$timeout','hotkeys','$rootScope',
+                    'egCore','$uibModal','ngToast','egOpChange','$element',
+            function($scope , $window , $location , $timeout , hotkeys , $rootScope ,
+                     egCore , $uibModal , ngToast , egOpChange , $element) {
 
-            // Find all eg-accesskey entries within the menu and attach
-            // hotkey handlers for each.  
-            // jqlite doesn't support selectors, so we have to 
-            // manually navigate to the elements we're interested in.
-            function inspect(elm) {
-                elm = angular.element(elm);
-                if (elm.attr('eg-accesskey')) {
-                    scope.addHotkey(
-                        elm.attr('eg-accesskey'),
-                        elm.attr('href'),
-                        elm.attr('eg-accesskey-desc'),
-                        elm
-                    );
-                }
-                angular.forEach(elm.children(), inspect);
-            }
-            inspect(element);
-        },
-
-        controller:['$scope','$window','$location','$timeout','hotkeys',
-                    'egCore','$uibModal','ngToast','egOpChange',
-            function($scope , $window , $location , $timeout , hotkeys ,
-                     egCore , $uibModal , ngToast, egOpChange) {
+                $scope.rs = $rootScope;
 
                 $scope.reprintLast = function (e) {
                     egCore.print.reprintLast();
                     return e.preventDefault();
                 }
 
-                function navTo(path) {                                           
-                    // Strip the leading "./" if any.
+                function navTo(path) {
                     path = path.replace(/^\.\//,'');
-                    var reg = new RegExp($location.path());
                     $window.location.href = egCore.env.basePath + path;
                 }       
 
                 // adds a keyboard shortcut
                 // http://chieffancypants.github.io/angular-hotkeys/
-                $scope.addHotkey = function(key, path, desc, elm) {                 
+                $scope.addHotkey = function(key, path, desc, elm) {
                     angular.forEach(key.split(' '), function (k) {
                         hotkeys.add({
                             combo: k,
@@ -53,13 +32,27 @@ angular.module('egCoreMod')
                             description: desc,
                             callback: function(e) {
                                 e.preventDefault();
-                                if (path) return navTo(path);
+                                if (path) return navTo(path,route);
                                 return $timeout(function(){$(elm).trigger('click')});
                             }
                         });
                     });
                 };
 
+                function inspect(elm) {
+                    elm = angular.element(elm);
+                    if (elm.attr('eg-accesskey')) {
+                        $scope.addHotkey(
+                            elm.attr('eg-accesskey'),
+                            elm.attr('href'),
+                            elm.attr('eg-accesskey-desc'),
+                            elm
+                        );
+                    }
+                    angular.forEach(elm.children(), inspect);
+                }
+                $timeout(function(){inspect($element)});
+
                 $scope.retrieveLastRecord = function() {
                     var last_record = egCore.hatch.getLocalItem("eg.cat.last_record_retrieved");
                     if (last_record) {
diff --git a/Open-ILS/web/js/ui/default/staff/services/org.js b/Open-ILS/web/js/ui/default/staff/services/org.js
index e9fcc2d..93efc44 100644
--- a/Open-ILS/web/js/ui/default/staff/services/org.js
+++ b/Open-ILS/web/js/ui/default/staff/services/org.js
@@ -18,8 +18,8 @@
 angular.module('egCoreMod')
 
 .factory('egOrg', 
-       ['$q','egEnv','egAuth','egNet',
-function($q,  egEnv,  egAuth,  egNet) { 
+       ['$q','egEnv','egAuth','egNet','$injector',
+function($q,  egEnv,  egAuth,  egNet , $injector) { 
 
     var service = {};
 
@@ -98,17 +98,34 @@ function($q,  egEnv,  egAuth,  egNet) {
         return list;
     }
 
+    var egLovefield = null;
     // returns a promise, resolved with a hash of setting name =>
     // setting value for the selected org unit.  Org unit defaults to 
     // auth workstation org unit.
     service.settings = function(names, ou_id) {
+        if (!egLovefield) {
+            egLovefield = $injector.get('egLovefield');
+        }
+
+        // allow non-array
+        if (!angular.isArray(names)) names = [names];
+
+        if (lf.isOffline) {
+            return egLovefield.getSettingsCache(names)
+                .then(function(settings) {
+                    var hash = {};
+                    angular.forEach(settings, function (s) {
+                        hash[s.name] = s.value;
+                    });
+                    return $q.when(hash);
+                });
+        }
+
         var deferred = $q.defer();
         ou_id = ou_id || egAuth.user().ws_ou();
         var here = (ou_id == egAuth.user().ws_ou());
 
-        // allow non-array
-        if (!angular.isArray(names)) names = [names];
-        
+       
         if (here) { 
             // only cache org settings retrieved for the current 
             // workstation org unit.
@@ -136,9 +153,11 @@ function($q,  egEnv,  egAuth,  egNet) {
                 if (here) service.cachedSettings[key] = settings[key];
             });
 
-            // resolve with cached settings if 'here', since 'settings'
-            // will only contain settings we had to retrieve
-            deferred.resolve(here ? service.cachedSettings : settings);
+            return egLovefield.setSettingsCache(settings).then(function() {
+                // resolve with cached settings if 'here', since 'settings'
+                // will only contain settings we had to retrieve
+                deferred.resolve(here ? service.cachedSettings : settings);
+            });
         });
         return deferred.promise;
     }
diff --git a/Open-ILS/web/js/ui/default/staff/services/print.js b/Open-ILS/web/js/ui/default/staff/services/print.js
index 03ea6b8..d6ddf59 100644
--- a/Open-ILS/web/js/ui/default/staff/services/print.js
+++ b/Open-ILS/web/js/ui/default/staff/services/print.js
@@ -58,9 +58,12 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
     service.fleshPrintScope = function(scope) {
         if (!scope) scope = {};
         scope.today = new Date().toISOString();
-        scope.staff = egIDL.toHash(egAuth.user());
-        scope.current_location = 
-            egIDL.toHash(egOrg.get(egAuth.user().ws_ou()));
+
+        if (!lf.isOffline) {
+            scope.staff = egIDL.toHash(egAuth.user());
+            scope.current_location = 
+                egIDL.toHash(egOrg.get(egAuth.user().ws_ou()));
+        }
 
         return service.fetch_includes(scope);
     }
diff --git a/Open-ILS/web/js/ui/default/staff/services/startup.js b/Open-ILS/web/js/ui/default/staff/services/startup.js
index 038eb2d..958ff9f 100644
--- a/Open-ILS/web/js/ui/default/staff/services/startup.js
+++ b/Open-ILS/web/js/ui/default/staff/services/startup.js
@@ -48,6 +48,8 @@ function($q,  $rootScope,  $location,  $window,  egIDL,  egAuth,  egEnv , egOrg
     // returns true if we are staying on the current page
     // false if we are redirecting to login
     service.expiredAuthHandler = function() {
+        if (lf.isOffline) return true; // Only set by the offline UI
+
         console.debug('egStartup.expiredAuthHandler()');
         egAuth.logout(); // clean up
 
diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js
index f6dc3a0..d1f87a7 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -695,8 +695,8 @@ function($window , egStrings) {
            + '</ul>'
           + '</div>',
 
-        controller : ['$scope','$timeout','egCore','egStartup',
-              function($scope , $timeout , egCore , egStartup) {
+        controller : ['$scope','$timeout','egCore','egStartup','egLovefield','$q',
+              function($scope , $timeout , egCore , egStartup , egLovefield , $q) {
 
             if ($scope.alldisabled) {
                 $scope.disable_button = $scope.alldisabled == 'true' ? true : false;
@@ -713,32 +713,40 @@ function($window , egStrings) {
             //
             // controller() runs before link().
             // This post-startup code runs after link().
-            egStartup.go().then(function() {
-
-                $scope.orgList = egCore.org.list().map(function(org) {
-                    return {
-                        id : org.id(),
-                        shortname : org.shortname(), 
-                        depth : org.ou_type().depth()
+            egStartup.go(
+            ).then(
+                function() {
+                    return egCore.env.classLoaders.aou();
+                }
+            ).then(
+                function() {
+
+                    $scope.orgList = egCore.org.list().map(function(org) {
+                        return {
+                            id : org.id(),
+                            shortname : org.shortname(), 
+                            depth : org.ou_type().depth()
+                        }
+                    });
+                    
+    
+                    // Apply default values
+    
+                    if ($scope.stickySetting) {
+                        var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
+                        if (orgId) {
+                            $scope.selected = egCore.org.get(orgId);
+                        }
                     }
-                });
-
-                // Apply default values
-
-                if ($scope.stickySetting) {
-                    var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
-                    if (orgId) {
-                        $scope.selected = egCore.org.get(orgId);
+    
+                    if (!$scope.selected && !$scope.nodefault && egCore.auth.user()) {
+                        $scope.selected = 
+                            egCore.org.get(egCore.auth.user().ws_ou());
                     }
+    
+                    fire_orgsel_onchange(); // no-op if nothing is selected
                 }
-
-                if (!$scope.selected && !$scope.nodefault) {
-                    $scope.selected = 
-                        egCore.org.get(egCore.auth.user().ws_ou());
-                }
-
-                fire_orgsel_onchange(); // no-op if nothing is selected
-            });
+            );
 
             /**
              * Fire onchange handler after a timeout, so the
@@ -787,6 +795,17 @@ function($window , egStrings) {
     }
 })
 
+.directive('nextOnEnter', function () {
+    return function (scope, element, attrs) {
+        element.bind("keydown keypress", function (event) {
+            if(event.which === 13) {
+                $('#'+attrs.nextOnEnter).focus();
+                event.preventDefault();
+            }
+        });
+    };
+})
+
 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
 .directive('egEnter', function () {
     return function (scope, element, attrs) {
@@ -810,18 +829,43 @@ function($window , egStrings) {
     function(egStrings, egCore) {
         return {
             scope : {
+                id : '@',
                 closeText : '@',
                 ngModel : '=',
                 ngChange : '=',
                 ngBlur : '=',
+                minDate : '=?',
+                maxDate : '=?',
                 ngDisabled : '=',
                 ngRequired : '=',
                 hideDatePicker : '=',
-                dateFormat : '=?'
+                dateFormat : '=?',
+                outOfRange : '=?'
             },
             require: 'ngModel',
             templateUrl: './share/t_datetime',
             replace: true,
+            controller : ['$scope', function($scope) {
+                $scope.options = {
+                    minDate : $scope.minDate,
+                    maxDate : $scope.maxDate
+                };
+
+                var maxDateObj = $scope.maxDate ? new Date($scope.maxDate) : null;
+                var minDateObj = $scope.minDate ? new Date($scope.minDate) : null;
+
+                if ($scope.outOfRange !== undefined && (maxDateObj || minDateObj)) {
+                    $scope.$watch('ngModel', function (n,o) {
+                        if (n && n != o) {
+                            var bad = false;
+                            var newdate = new Date(n);
+                            if (maxDateObj && newdate.getTime() > maxDateObj.getTime()) bad = true;
+                            if (minDateObj && newdate.getTime() < minDateObj.getTime()) bad = true;
+                            $scope.outOfRange = bad;
+                        }
+                    });
+                }
+            }],
             link : function(scope, elm, attrs) {
                 if (!scope.closeText)
                     scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
diff --git a/Open-ILS/web/upup.min.js b/Open-ILS/web/upup.min.js
new file mode 100644
index 0000000..c28fe99
--- /dev/null
+++ b/Open-ILS/web/upup.min.js
@@ -0,0 +1,7 @@
+//! UpUp
+//! version : 0.3.0
+//! author  : Tal Ater @TalAter
+//! license : MIT
+//! https://github.com/TalAter/UpUp
+(function(a){"use strict";var b=this,c=navigator.serviceWorker;if(!c)return b.UpUp=null,a;var d={"service-worker-url":"upup.sw.min.js"},e=!1;b.UpUp={start:function(a){this.addSettings(a),c.register(d["service-worker-url"],{scope:"./"}).then(function(a){e&&console.log("Service worker registration successful with scope: %c"+a.scope,"font-weight: bold; color: #00f;"),(a.installing||c.controller).postMessage({action:"set-settings",settings:d})}).catch(function(a){e&&console.log("Service worker registration failed: %c"+a,"font-weight: bold; color: #00f;")})},addSettings:function(b){b=b||{},"string"==typeof b&&(b={content:b}),["content","content-url","assets","service-worker-url","cache-version"].forEach(function(c){b[c]!==a&&(d[c]=b[c])})},debug:function(a){e=!(arguments.length>0)||!!a}}}).call(this);
+//# sourceMappingURL=upup.min.js.map
\ No newline at end of file
diff --git a/Open-ILS/web/upup.sw.min.js b/Open-ILS/web/upup.sw.min.js
new file mode 100644
index 0000000..87f2ece
--- /dev/null
+++ b/Open-ILS/web/upup.sw.min.js
@@ -0,0 +1,7 @@
+//! UpUp Service Worker
+//! version : 0.3.0
+//! author  : Tal Ater @TalAter
+//! license : MIT
+//! https://github.com/TalAter/UpUp
+var _CACHE_NAME_PREFIX="upup-cache",_calculateHash=function(a){a=a.toString();var b,c,d=0,e=a.length;if(0===e)return d;for(b=0;b<e;b++)c=a.charCodeAt(b),d=(d<<5)-d+c,d|=0;return d};self.addEventListener("message",function(a){"set-settings"===a.data.action&&_parseSettingsAndCache(a.data.settings)}),self.addEventListener("fetch",function(a){a.respondWith(fetch(a.request).catch(function(){return caches.match(a.request).then(function(b){return b||("navigate"===a.request.mode||"GET"===a.request.method&&a.request.headers.get("accept").includes("text/html")?caches.match("sw-offline-content"):void 0)})}))});var _parseSettingsAndCache=function(a){var b=_CACHE_NAME_PREFIX+"-"+(a["cache-version"]?a["cache-version"]+"-":"")+_calculateHash(a.content+a["content-url"]+a.assets);return caches.open(b).then(function(b){return a.assets&&b.addAll(a.assets.map(function(a){return new Request(a,{mode:"no-cors"})})),a["content-url"]?fetch(a["content-url"],{mode:"no-cors"}).then(function(a){return b
 .put("sw-offline-content",a)}):a.content?b.put("sw-offline-content",_buildResponse(a.content)):b.put("sw-offline-content",_buildResponse("You are offline"))}).then(function(){return caches.keys().then(function(a){return Promise.all(a.map(function(a){if(a.startsWith(_CACHE_NAME_PREFIX)&&b!==a)return caches.delete(a)}))})})},_buildResponse=function(a){return new Response(a,{headers:{"Content-Type":"text/html"}})};
+//# sourceMappingURL=upup.sw.min.js.map
\ No newline at end of file

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/src/offline/offline.pl                    |   43 +-
 .../src/perlmods/lib/OpenILS/WWW/Proxy/Authen.pm   |    4 +-
 .../src/perlmods/live_t/24-offline-all-assets.t    |    8 +
 .../staff/admin/workstation/t_print_templates.tt2  |    4 +
 Open-ILS/src/templates/staff/base_js.tt2           |   92 +
 .../templates/staff/circ/patron/reg_actions.tt2    |    2 +-
 .../src/templates/staff/circ/patron/t_edit.tt2     |   14 +-
 Open-ILS/src/templates/staff/config.tt2            |    4 +-
 Open-ILS/src/templates/staff/index.tt2             |    1 +
 Open-ILS/src/templates/staff/navbar.tt2            |   57 +-
 Open-ILS/src/templates/staff/offline-interface.tt2 |  632 ++++++
 .../share/print_templates/t_offline_checkin.tt2    |   25 +
 .../share/print_templates/t_offline_checkout.tt2   |   26 +
 .../print_templates/t_offline_in_house_use.tt2     |   24 +
 .../share/print_templates/t_offline_renew.tt2      |   24 +
 Open-ILS/src/templates/staff/share/t_datetime.tt2  |    6 +
 Open-ILS/src/templates/staff/t_login.tt2           |    6 +
 Open-ILS/web/LICENSE.UpUp                          |   21 +
 Open-ILS/web/js/ui/default/staff/Gruntfile.js      |   22 +-
 Open-ILS/web/js/ui/default/staff/app.js            |    8 +-
 .../web/js/ui/default/staff/circ/patron/regctl.js  |   31 +-
 Open-ILS/web/js/ui/default/staff/offline.js        | 2119 ++++++++++++++++++++
 Open-ILS/web/js/ui/default/staff/package.json      |    1 +
 Open-ILS/web/js/ui/default/staff/services/auth.js  |   44 +-
 Open-ILS/web/js/ui/default/staff/services/env.js   |   97 +-
 Open-ILS/web/js/ui/default/staff/services/file.js  |   58 +-
 Open-ILS/web/js/ui/default/staff/services/hatch.js |   36 +
 Open-ILS/web/js/ui/default/staff/services/idl.js   |   62 +-
 .../web/js/ui/default/staff/services/lovefield.js  |  406 ++++
 .../web/js/ui/default/staff/services/navbar.js     |   51 +-
 Open-ILS/web/js/ui/default/staff/services/org.js   |   35 +-
 Open-ILS/web/js/ui/default/staff/services/print.js |    9 +-
 .../web/js/ui/default/staff/services/startup.js    |    2 +
 Open-ILS/web/js/ui/default/staff/services/ui.js    |   96 +-
 .../web/js/ui/default/staff/test/karma.conf.js     |    2 +
 Open-ILS/web/upup.min.js                           |    7 +
 Open-ILS/web/upup.sw.min.js                        |    7 +
 37 files changed, 3884 insertions(+), 202 deletions(-)
 create mode 100644 Open-ILS/src/perlmods/live_t/24-offline-all-assets.t
 create mode 100644 Open-ILS/src/templates/staff/offline-interface.tt2
 create mode 100644 Open-ILS/src/templates/staff/share/print_templates/t_offline_checkin.tt2
 create mode 100644 Open-ILS/src/templates/staff/share/print_templates/t_offline_checkout.tt2
 create mode 100644 Open-ILS/src/templates/staff/share/print_templates/t_offline_in_house_use.tt2
 create mode 100644 Open-ILS/src/templates/staff/share/print_templates/t_offline_renew.tt2
 create mode 100644 Open-ILS/web/LICENSE.UpUp
 create mode 100644 Open-ILS/web/js/ui/default/staff/offline.js
 create mode 100644 Open-ILS/web/js/ui/default/staff/services/lovefield.js
 create mode 100644 Open-ILS/web/upup.min.js
 create mode 100644 Open-ILS/web/upup.sw.min.js


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list