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

Evergreen Git git at git.evergreen-ils.org
Thu Aug 18 15:36:42 EDT 2016


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  4e4525c85e795a42231ffbb0e8e10c027d50a682 (commit)
       via  3d78e822af3a6337430825dcf328889ea254c6b6 (commit)
       via  059411ba21bb1dffb2d2db351ce1eddfc540fb8f (commit)
       via  350500afb00a0a2b244ef93d5cabb9549ffa59cd (commit)
       via  f6eeb168a73ac86506890997ae6f426d8ddaedbe (commit)
       via  997a30a6973b9161c7336a086534c41810665f50 (commit)
       via  d78e8a4f5e3a9fa501573e7f26bf69de566632ad (commit)
       via  b1f1f64b932bf113c7e6ddf80ee66b892dc6fdfd (commit)
       via  804a907f4b21132c2b5c7865eed955df2a5e3814 (commit)
       via  59af472f0726b7ffdc89c66dab685fd3e686c9f7 (commit)
       via  541921ab1de0a45002a501a2bea545c47f2c5252 (commit)
       via  461eef4e0bd4caec45adafdce2ec9c8095a24256 (commit)
       via  d72697057908bbbecc961cc9a0eae421ab688c7d (commit)
       via  60c2eb3b9137c7b07c1bdbb7c1cbd1ab598d0b5e (commit)
       via  a2d1ab854f916606d9af3ce991c9601ff8c9897f (commit)
       via  4e911171a113e5d7562ed998229f108a9120badc (commit)
       via  872c8c2f0d98016fc1639dcc0d49300ae03e1660 (commit)
       via  aca3dd932957a062eb5f6385bb89d3dfae08d011 (commit)
       via  d914aab30ba0ccf75a081aa8dfcf157e8385c1e1 (commit)
       via  297a0b142326320f54f83b1f3d9d7f7c9a0b495a (commit)
       via  efa65bd5c7640758585070e66819f06e82522b78 (commit)
       via  4e93a8bd0d240a10ec545c927ca92864efba95e9 (commit)
       via  3bf01639f186522674cd6d1f347b713f0d5766e8 (commit)
       via  eabda938c992ae2269ec793eefd2ec0980f100ed (commit)
       via  d271b796cebc71e916e85249c15c2484444a390b (commit)
       via  2a8148341428dc0fc1c4947a148e5d996bc6db6b (commit)
       via  bface48e0fff0512d46dee412bc46e8454521c5a (commit)
       via  28f7159c31279217d01258eb85d662fad2785680 (commit)
       via  676fdff4e78bcf33eb4f8c488d8995fcf9f75f65 (commit)
       via  d02c84fce8a3e99ce7be5357277b5958d8b04183 (commit)
       via  eb53b50cf844ba7bfe3bf5f069cd6a9d80be868e (commit)
       via  cea022c407e4193dcf3fed6b9ca81edfe04c3038 (commit)
       via  f20039da9caa3e43bf626b65b667516e4383b0d5 (commit)
       via  cce69273995f6f6668e5f5a0c52cafd4ed20f54d (commit)
       via  7292174f182bba027362946c0a4a47d3ba2534da (commit)
       via  84df55949e502b16c724fdfdcc8dcafa59dedd28 (commit)
       via  51fe14771d2f117f0f980047f53a64772f2c2ba1 (commit)
       via  4e1a8f8245d649e8574b0357f5e79f92414bba3b (commit)
       via  7482a039798fb9f71a64562b4a94d05be4db0ff8 (commit)
       via  bc5aebd7b20b53b24fa2c5c7eb066d7d21cb3689 (commit)
       via  f3931174b5257fa1574b298c3608839bdcddaf12 (commit)
      from  9575feec7189a9193b561de789206df32c92f996 (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 4e4525c85e795a42231ffbb0e8e10c027d50a682
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Wed Aug 17 10:53:19 2016 -0400

    LP#1476049: disable serve-cgi-bin Apache config on Jessie and Xenial
    
    This patch ensures that the stock Apache configuration
    "serve-cgi-bin" is disabled when installing on Debian Jessie
    and Ubuntu Xenial, as otherwise the default cgi-bin location
    can override what Evergreen specifies for offline.pl.
    
    To test:
    
    [1] Run the Makefile.install step on Jessie or Xenial; verify
        that there is no /etc/apache2/conf-enabled/serve-cgi-bin.conf
        symlink.
    [2] Proceed with the rest of the Evergreen installation; verify
        that the Apache error log doesn't contain a warning like this:
    
        "The Alias directive in /etc/apache2/sites-enabled/eg.conf ...
         will probably never match because it overlaps an earlier
         ScriptAlias."
    
    [3] For extra credit, verify that one can access offline circulation
        sessions.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/extras/install/Makefile.debian-jessie b/Open-ILS/src/extras/install/Makefile.debian-jessie
index 030857c..e76a60e 100644
--- a/Open-ILS/src/extras/install/Makefile.debian-jessie
+++ b/Open-ILS/src/extras/install/Makefile.debian-jessie
@@ -85,6 +85,9 @@ export DEB_APACHE_MODS = \
 export DEB_APACHE_DISMODS = \
     deflate
 
+export DEB_APACHE_DISCONF = \
+    serve-cgi-bin
+
 export CPAN_MODULES = \
 	Business::OnlinePayment::PayPal \
 	Email::Send
diff --git a/Open-ILS/src/extras/install/Makefile.ubuntu-xenial b/Open-ILS/src/extras/install/Makefile.ubuntu-xenial
index 726fdbb..ae37fe0 100644
--- a/Open-ILS/src/extras/install/Makefile.ubuntu-xenial
+++ b/Open-ILS/src/extras/install/Makefile.ubuntu-xenial
@@ -84,6 +84,9 @@ export DEB_APACHE_MODS = \
 export DEB_APACHE_DISMODS = \
     deflate
 
+export DEB_APACHE_DISCONF = \
+    serve-cgi-bin
+
 export CPAN_MODULES = \
 	Business::OnlinePayment::PayPal \
 	Email::Send

commit 3d78e822af3a6337430825dcf328889ea254c6b6
Author: Michele Morgan <mmorgan at noblenet.org>
Date:   Tue Aug 16 14:47:43 2016 -0400

    LP#1583729 Item status screen column options do not include age protection
    
    Adds the age_protect field to several missing copy interfaces:
    
    XUL client:
    
    - Item status list view column picker
    - Item status alternate view
    - Holdings Maintenance column picker
    
    Web client:
    
    - Item status alternate view
    - Holdings view column picker
    
    Also addresses an issue with displaying the circ modifier in some item
    interfaces.
    
    Signed-off-by: Michele Morgan <mmorgan at noblenet.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
index 8bbfb45..bd618ac 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
@@ -109,6 +109,7 @@
     <eg-grid-field label="[% l('Circulate As MARC Type') %]" path="circ_as_type"></eg-grid-field>
     <eg-grid-field label="[% l('Circulate') %]"              datatype="bool" path="circulate"></eg-grid-field>
     <eg-grid-field label="[% l('Holdable') %]"               datatype="bool" path="holdable"></eg-grid-field>
+    <eg-grid-field label="[% l('Age-based Hold Protection') %]" path="age_protect.name"></eg-grid-field>
     <eg-grid-field label="[% l('Reference') %]"              datatype="bool" path="ref"></eg-grid-field>
   
   </eg-grid>
diff --git a/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2 b/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2
index c21c9cd..1ab16a5 100644
--- a/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2
+++ b/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2
@@ -161,6 +161,8 @@
     <div class="flex-cell">[% l('Circ Modifier') %]</div>
     <div class="flex-cell well">{{copy.circ_modifier().name()}}</div>
 
+    <div class="flex-cell">[% l('Age-based Hold Protection') %]</div>
+    <div class="flex-cell well">{{copy.age_protect().name()}}</div>
     <!-- empty -->
     <div class="flex-cell"></div>
     <div class="flex-cell"></div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/item/app.js b/Open-ILS/web/js/ui/default/staff/cat/item/app.js
index a8781dc..922a531 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/item/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/item/app.js
@@ -59,7 +59,7 @@ function(egCore) {
     service.flesh = {   
         flesh : 3, 
         flesh_fields : {
-            acp : ['call_number','location','status','location','floating'],
+            acp : ['call_number','location','status','location','floating','circ_modifier','age_protect'],
             acn : ['record','prefix','suffix'],
             bre : ['simple_record','creator','editor']
         },
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/holdings.js b/Open-ILS/web/js/ui/default/staff/cat/services/holdings.js
index 2742915..71255a4 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/holdings.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/holdings.js
@@ -15,7 +15,7 @@ function(egCore , $q) {
     service.prototype.flesh = {   
         flesh : 2, 
         flesh_fields : {
-            acp : ['status','location','circ_lib','parts'],
+            acp : ['status','location','circ_lib','parts','age_protect'],
             acn : ['prefix','suffix','copies']
         }
     }
diff --git a/Open-ILS/web/opac/locale/en-US/lang.dtd b/Open-ILS/web/opac/locale/en-US/lang.dtd
index bbdf8ac..3dd1b4a 100644
--- a/Open-ILS/web/opac/locale/en-US/lang.dtd
+++ b/Open-ILS/web/opac/locale/en-US/lang.dtd
@@ -3753,6 +3753,7 @@
 <!ENTITY staff.circ.alternate_copy_summary.Edition.label "Edition">
 <!ENTITY staff.circ.alternate_copy_summary.Floating.label "Floating">
 <!ENTITY staff.circ.alternate_copy_summary.Holdable.label "Holdable">
+<!ENTITY staff.circ.alternate_copy_summary.Age_Protect.label "Age-based Hold Protection">
 <!ENTITY staff.circ.alternate_copy_summary.Hold_Shelf_Location.label "Hold Shelf Location">
 <!ENTITY staff.circ.alternate_copy_summary.Holds_Transit.label "Holds/Transit">
 <!ENTITY staff.circ.alternate_copy_summary.Holds_Transit.accesskey "">
diff --git a/Open-ILS/xul/staff_client/server/cat/copy_browser.js b/Open-ILS/xul/staff_client/server/cat/copy_browser.js
index 532ec1b..327b45b 100644
--- a/Open-ILS/xul/staff_client/server/cat/copy_browser.js
+++ b/Open-ILS/xul/staff_client/server/cat/copy_browser.js
@@ -1758,6 +1758,7 @@ cat.copy_browser.prototype = {
                             'fine_level',
                             'circulate',
                             'holdable',
+                            'age_protect',
                             'opac_visible',
                             'ref',
                             'deposit',
diff --git a/Open-ILS/xul/staff_client/server/circ/alternate_copy_summary.js b/Open-ILS/xul/staff_client/server/circ/alternate_copy_summary.js
index 74b7ae3..43c52ff 100644
--- a/Open-ILS/xul/staff_client/server/circ/alternate_copy_summary.js
+++ b/Open-ILS/xul/staff_client/server/circ/alternate_copy_summary.js
@@ -191,7 +191,12 @@ function load_item() {
 
         if (details.copy) {
             set("stat_cat_entries", details.copy.stat_cat_entries()); 
-            set("age_protect", details.copy.age_protect()); 
+            var ap = details.copy.age_protect();
+            if (typeof data.hash.crahp[ap] != 'undefined') {
+                set("age_protect", data.lookup('crahp',details.copy.age_protect()).name());
+            } else {
+                set("age_protect","");
+            }
             set("alert_message", details.copy.alert_message()); 
             set("barcode", details.copy.barcode()); 
             if (typeof details.copy.call_number() == 'object') {
diff --git a/Open-ILS/xul/staff_client/server/circ/alternate_copy_summary.xul b/Open-ILS/xul/staff_client/server/circ/alternate_copy_summary.xul
index 3c4ba95..a06ea95 100644
--- a/Open-ILS/xul/staff_client/server/circ/alternate_copy_summary.xul
+++ b/Open-ILS/xul/staff_client/server/circ/alternate_copy_summary.xul
@@ -158,7 +158,8 @@
                                 <textbox name="copy_id" readonly="true" context="clipboard"/>
                                 <label value="&staff.circ.alternate_copy_summary.Holdable.label;" />
                                 <textbox name="holdable" readonly="true" context="clipboard"/>
-                                <spacer /><spacer />
+                                <label value="&staff.circ.alternate_copy_summary.Age_Protect.label;" />
+                                <textbox name="age_protect" readonly="true" context="clipboard"/>
                                 <label value="&staff.circ.alternate_copy_summary.Checkin_Time.label;" />
                                 <textbox name="checkin_time" readonly="true" context="clipboard"/>
                             </row>
diff --git a/Open-ILS/xul/staff_client/server/circ/util.js b/Open-ILS/xul/staff_client/server/circ/util.js
index 3f473e8..9d372d8 100644
--- a/Open-ILS/xul/staff_client/server/circ/util.js
+++ b/Open-ILS/xul/staff_client/server/circ/util.js
@@ -922,6 +922,21 @@ circ.util.columns = function(modify,params) {
             }
         },
         {
+            'id' : 'age_protect',
+            'fm_class' : 'acp',
+            'label' : document.getElementById('circStrings').getString('staff.circ.utils.age_protect'),
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : true,
+            'editable' : false, 'render' : function(my) {
+                if (Number(my.acp.age_protect())>=0) {
+                    return data.lookup("crahp", my.acp.age_protect() ).name();
+                } else {
+                    return my.acp.age_protect().name();
+                }
+            }
+        },
+        {
             'id' : 'floating',
             'fm_class' : 'acp',
             'label' : document.getElementById('circStrings').getString('staff.circ.utils.floating'),
diff --git a/Open-ILS/xul/staff_client/server/locale/en-US/circ.properties b/Open-ILS/xul/staff_client/server/locale/en-US/circ.properties
index d5bb8d1..0e6cd05 100644
--- a/Open-ILS/xul/staff_client/server/locale/en-US/circ.properties
+++ b/Open-ILS/xul/staff_client/server/locale/en-US/circ.properties
@@ -256,6 +256,7 @@ staff.circ.utils.fine_level.high=High
 staff.circ.utils.circulate=Circulate?
 staff.circ.utils.deleted=Deleted?
 staff.circ.utils.holdable=Holdable?
+staff.circ.utils.age_protect=Age-based Hold Protection
 staff.circ.utils.floating=Floating?
 staff.circ.utils.hold_note=Hold Note(s) Count
 staff.circ.utils.hold_note_text=Hold Note(s) Text
diff --git a/Open-ILS/xul/staff_client/server/locale/en-US/common.properties b/Open-ILS/xul/staff_client/server/locale/en-US/common.properties
index 4717683..78ce724 100644
--- a/Open-ILS/xul/staff_client/server/locale/en-US/common.properties
+++ b/Open-ILS/xul/staff_client/server/locale/en-US/common.properties
@@ -48,6 +48,7 @@ staff.acp_label_location=Location
 staff.acp_label_price=Price
 staff.acp_label_cost=Acquisition Cost
 staff.acp_label_status=Status
+staff.acp_label_age_protect=Age-based Hold Protection
 staff.ahr_current_copy_label=Current Copy
 staff.ahr_current_copy_location_label=Current Copy Location
 staff.ahr_email_notify_label=Email Notify

commit 059411ba21bb1dffb2d2db351ce1eddfc540fb8f
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Aug 5 12:39:54 2016 -0400

    LP#1527694 Webstaff egHatch supports 'LoginSession' storage
    
    Adds support for a class of cached value (AKA "LoginSession" items)
    that are cleared when either the user logs out or the browser is closed.
    Values are stored as cookies.
    
    Authentication tokens and "retrieve last patron" data are now stored as
    "LoginSession" items.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js
index d31afa2..fe13939 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js
@@ -207,8 +207,8 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap',
  * Patron service
  */
 .factory('patronSvc',
-       ['$q','$timeout','$location','$cookies','egCore','egUser','$locale',
-function($q , $timeout , $location , $cookies , egCore,  egUser , $locale) {
+       ['$q','$timeout','$location','egCore','egUser','$locale',
+function($q , $timeout , $location , egCore,  egUser , $locale) {
 
     var service = {
         // cached patron search results
@@ -271,7 +271,7 @@ function($q , $timeout , $location , $cookies , egCore,  egUser , $locale) {
 
         // when loading a new patron, update the last patron setting
         if (!service.current || service.current.id() != user_id)
-            $cookies.put('eg.circ.last_patron', user_id);
+            egCore.hatch.setLoginSessionItem('eg.circ.last_patron', user_id);
 
         // avoid running multiple retrievals for the same patron, which
         // can happen during dbl-click by maintaining a single running
@@ -1553,10 +1553,10 @@ function($scope,  $routeParams , $q , egCore , patronSvc) {
 }])
 
 .controller('PatronFetchLastCtrl',
-       ['$scope','$location','$cookies','egCore',
-function($scope , $location , $cookies, egCore) {
+       ['$scope','$location','egCore',
+function($scope , $location , egCore) {
 
-    var id = $cookies.get('eg.circ.last_patron');
+    var id = egCore.hatch.getLoginSessionItem('eg.circ.last_patron');
     if (id) return $location.path('/circ/patron/' + id + '/checkout');
 
     $scope.no_last = true;
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 75ae9ba..a677ff4 100644
--- a/Open-ILS/web/js/ui/default/staff/services/auth.js
+++ b/Open-ILS/web/js/ui/default/staff/services/auth.js
@@ -17,12 +17,12 @@ function($q , $timeout , $rootScope , egNet , egHatch) {
 
         // the currently active auth token string
         token : function() {
-            return egHatch.getLocalItem('eg.auth.token');
+            return egHatch.getLoginSessionItem('eg.auth.token');
         },
 
         // authtime in seconds
         authtime : function() {
-            return egHatch.getLocalItem('eg.auth.time');
+            return egHatch.getLoginSessionItem('eg.auth.time');
         },
 
         // the currently active workstation name
@@ -68,7 +68,7 @@ function($q , $timeout , $rootScope , egNet , egHatch) {
                     }
                 } else {
                     // authtoken test failed
-                    egHatch.removeLocalItem('eg.auth.token');
+                    egHatch.clearLoginSessionItems();
                     deferred.reject(); 
                 }
             });
@@ -87,6 +87,17 @@ function($q , $timeout , $rootScope , egNet , egHatch) {
      */
     service.login = function(args) {
         var deferred = $q.defer();
+
+        // Clear old LoginSession keys that were left in localStorage
+        // when the previous user closed the browser without logging
+        // out.  Under normal circumstance, LoginSession data would
+        // have been cleared by now, either during logout or cookie
+        // expiration.  But, if for some reason the user manually
+        // removed the auth token cookie w/o closing the browser
+        // (say, for testing), then this serves double duty to ensure
+        // LoginSession data cannot persist across logins.
+        egHatch.clearLoginSessionItems();
+
         egNet.request(
             'open-ils.auth',
             'open-ils.auth.authenticate.init', args.username).then(
@@ -99,9 +110,9 @@ function($q , $timeout , $rootScope , egNet , egHatch) {
                         if (evt.textcode == 'SUCCESS') {
                             service.ws = args.workstation; 
                             service.poll();
-                            egHatch.setLocalItem(
+                            egHatch.setLoginSessionItem(
                                 'eg.auth.token', evt.payload.authtoken);
-                            egHatch.setLocalItem(
+                            egHatch.setLoginSessionItem(
                                 'eg.auth.time', evt.payload.authtime);
                             deferred.resolve();
                         } else {
@@ -157,8 +168,7 @@ function($q , $timeout , $rootScope , egNet , egHatch) {
                 'open-ils.auth', 
                 'open-ils.auth.session.delete', 
                 service.token()); // fire and forget
-            egHatch.removeLocalItem('eg.auth.token');
-            egHatch.removeLocalItem('eg.auth.time');
+            egHatch.clearLoginSessionItems();
         }
         service._user = null;
     };
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 0b8935b..ff517fc 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -25,8 +25,8 @@
 angular.module('egCoreMod')
 
 .factory('egHatch',
-           ['$q','$window','$timeout','$interpolate','$http',
-    function($q , $window , $timeout , $interpolate , $http) {
+           ['$q','$window','$timeout','$interpolate','$http','$cookies',
+    function($q , $window , $timeout , $interpolate , $http , $cookies) {
 
     var service = {};
     service.msgId = 0;
@@ -301,14 +301,25 @@ angular.module('egCoreMod')
         return JSON.parse(val);
     }
 
+    service.getLoginSessionItem = function(key) {
+        var val = $cookies.get(key);
+        if (val == null) return;
+        return JSON.parse(val);
+    }
+
     service.getSessionItem = function(key) {
         var val = $window.sessionStorage.getItem(key);
         if (val == null) return;
         return JSON.parse(val);
     }
 
+    /**
+     * @param tmp bool Store the value as a session cookie only.  
+     * tmp values are removed during logout or browser close.
+     */
     service.setItem = function(key, value) {
         var str = JSON.stringify(value);
+
         return service.setRemoteItem(key, str)['catch'](
             function(msg) {
                 if (service.hatchRequired()) {
@@ -330,7 +341,8 @@ angular.module('egCoreMod')
         });
     }
 
-    // Set the value for the given key
+    // Set the value for the given key.
+    // "Local" items persist indefinitely.
     // If the value is raw, pass it as 'value'.  If it was
     // externally JSONified, pass it via jsonified.
     service.setLocalItem = function(key, value, jsonified) {
@@ -339,6 +351,23 @@ angular.module('egCoreMod')
         $window.localStorage.setItem(key, jsonified);
     }
 
+    // Set the value for the given key.  
+    // "LoginSession" items are removed when the user logs out or the 
+    // browser is closed.
+    // If the value is raw, pass it as 'value'.  If it was
+    // externally JSONified, pass it via jsonified.
+    service.setLoginSessionItem = function(key, value, jsonified) {
+        service.addLoginSessionKey(key);
+        if (jsonified === undefined ) 
+            jsonified = JSON.stringify(value);
+        $cookies.put(key, jsonified);
+    }
+
+    // Set the value for the given key.  
+    // "Session" items are browser tab-specific and are removed when the
+    // tab is closed.
+    // If the value is raw, pass it as 'value'.  If it was
+    // externally JSONified, pass it via jsonified.
     service.setSessionItem = function(key, value, jsonified) {
         if (jsonified === undefined ) 
             jsonified = JSON.stringify(value);
@@ -403,10 +432,27 @@ angular.module('egCoreMod')
         $window.localStorage.removeItem(key);
     }
 
+    service.removeLoginSessionItem = function(key) {
+        service.removeLoginSessionKey(key);
+        $cookies.remove(key);
+    }
+
     service.removeSessionItem = function(key) {
         $window.sessionStorage.removeItem(key);
     }
 
+    /**
+     * Remove all "LoginSession" items.
+     */
+    service.clearLoginSessionItems = function() {
+        angular.forEach(service.getLoginSessionKeys(), function(key) {
+            service.removeLoginSessionItem(key);
+        });
+
+        // remove the keys cache.
+        service.removeLocalItem('eg.hatch.login_keys');
+    }
+
     // if set, prefix limits the return set to keys starting with 'prefix'
     service.getKeys = function(prefix) {
         return service.getRemoteKeys(prefix)['catch'](
@@ -439,6 +485,42 @@ angular.module('egCoreMod')
         return keys;
     }
 
+
+    /**
+     * Array of "LoginSession" keys.
+     * Note we have to store these as "Local" items so browser tabs can
+     * share them.  We could store them as cookies, but it's more data
+     * that has to go back/forth to the server.  A "LoginSession" key name is
+     * not private, though, so it's OK if they are left in localStorage
+     * until the next login.
+     */
+    service.getLoginSessionKeys = function(prefix) {
+        var keys = [];
+        var idx = 0;
+        var login_keys = service.getLocalItem('eg.hatch.login_keys') || [];
+        angular.forEach(login_keys, function(k) {
+            // key prefix match test
+            if (prefix && k.substr(0, prefix.length) != prefix) return;
+            keys.push(k);
+        });
+        return keys;
+    }
+
+    service.addLoginSessionKey = function(key) {
+        var keys = service.getLoginSessionKeys();
+        if (keys.indexOf(key) < 0) {
+            keys.push(key);
+            service.setLocalItem('eg.hatch.login_keys', keys);
+        }
+    }
+
+    service.removeLoginSessionKey = function(key) {
+        var keys = service.getLoginSessionKeys().filter(function(k) {
+            return k != key;
+        });
+        service.setLocalItem('eg.hatch.login_keys', keys);
+    }
+
     return service;
 }])
 

commit 350500afb00a0a2b244ef93d5cabb9549ffa59cd
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Aug 4 12:34:56 2016 -0400

    LP#1527694 Webstaff clear last patron
    
    Store most recent patron ID via cookie instead of localStorage so the
    value can expire when then browser is closed.
    
    Adds angular-cookies (ngCookies) dependency.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    
    Conflicts:
    	Open-ILS/web/js/ui/default/staff/bower.json

diff --git a/Open-ILS/src/templates/staff/base_js.tt2 b/Open-ILS/src/templates/staff/base_js.tt2
index bd3928f..af92b80 100644
--- a/Open-ILS/src/templates/staff/base_js.tt2
+++ b/Open-ILS/src/templates/staff/base_js.tt2
@@ -12,6 +12,7 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-location-update.min.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-animate.min.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-sanitize.min.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-cookies.min.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/ngToast.min.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/angular-tree-control.js"></script>
 
diff --git a/Open-ILS/web/js/ui/default/staff/Gruntfile.js b/Open-ILS/web/js/ui/default/staff/Gruntfile.js
index 910d7e1..4bfb433 100644
--- a/Open-ILS/web/js/ui/default/staff/Gruntfile.js
+++ b/Open-ILS/web/js/ui/default/staff/Gruntfile.js
@@ -31,6 +31,8 @@ module.exports = function(grunt) {
             'bower_components/angular-tree-control/angular-tree-control.js',
             'bower_components/ngtoast/dist/ngToast.min.js',
             'bower_components/jquery/dist/jquery.min.js',
+            'bower_components/angular-cookies/angular-cookies.min.js',
+            'bower_components/angular-cookies/angular-cookies.min.js.map'
           ]
         }]
       },
diff --git a/Open-ILS/web/js/ui/default/staff/bower.json b/Open-ILS/web/js/ui/default/staff/bower.json
index dcadeee..f332f3b 100644
--- a/Open-ILS/web/js/ui/default/staff/bower.json
+++ b/Open-ILS/web/js/ui/default/staff/bower.json
@@ -29,7 +29,8 @@
     "ngtoast": "~2.0.0",
     "angular-tree-control": "~0.2.28",
     "angular-animate": "~1.5.3",
-    "angular-hotkeys": "cfp-angular-hotkeys#^1.7.0"
+    "angular-hotkeys": "cfp-angular-hotkeys#^1.7.0",
+    "angular-cookies": "^1.5.8"
   },
   "resolutions": {
     "angular": "~1.5.5"
diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js
index 652a35f..d31afa2 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js
@@ -207,8 +207,8 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap',
  * Patron service
  */
 .factory('patronSvc',
-       ['$q','$timeout','$location','egCore','egUser','$locale',
-function($q , $timeout , $location , egCore,  egUser , $locale) {
+       ['$q','$timeout','$location','$cookies','egCore','egUser','$locale',
+function($q , $timeout , $location , $cookies , egCore,  egUser , $locale) {
 
     var service = {
         // cached patron search results
@@ -271,7 +271,7 @@ function($q , $timeout , $location , egCore,  egUser , $locale) {
 
         // when loading a new patron, update the last patron setting
         if (!service.current || service.current.id() != user_id)
-            egCore.hatch.setLocalItem('eg.circ.last_patron', user_id);
+            $cookies.put('eg.circ.last_patron', user_id);
 
         // avoid running multiple retrievals for the same patron, which
         // can happen during dbl-click by maintaining a single running
@@ -1553,10 +1553,10 @@ function($scope,  $routeParams , $q , egCore , patronSvc) {
 }])
 
 .controller('PatronFetchLastCtrl',
-       ['$scope','$location','egCore',
-function($scope , $location , egCore) {
+       ['$scope','$location','$cookies','egCore',
+function($scope , $location , $cookies, egCore) {
 
-    var id = egCore.hatch.getLocalItem('eg.circ.last_patron');
+    var id = $cookies.get('eg.circ.last_patron');
     if (id) return $location.path('/circ/patron/' + id + '/checkout');
 
     $scope.no_last = true;
diff --git a/Open-ILS/web/js/ui/default/staff/services/core.js b/Open-ILS/web/js/ui/default/staff/services/core.js
index 94c4b46..dc5ef6c 100644
--- a/Open-ILS/web/js/ui/default/staff/services/core.js
+++ b/Open-ILS/web/js/ui/default/staff/services/core.js
@@ -3,4 +3,4 @@
  * egCoreMod houses all of the services, etc. required by all pages
  * for basic functionality.
  */
-angular.module('egCoreMod', ['cfp.hotkeys', 'ngFileSaver']);
+angular.module('egCoreMod', ['cfp.hotkeys', 'ngFileSaver', 'ngCookies']);
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 760fc3b..b8c1259 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
@@ -12,6 +12,7 @@ module.exports = function(config){
       'bower_components/angular-file-saver/dist/angular-file-saver.bundle.min.js',
       'build/js/ui-bootstrap.min.js',
       'build/js/hotkeys.min.js',
+      'build/js/angular-cookies.min.js',
       /* OpenSRF must be installed first */
       '/openils/lib/javascript/md5.js',
       '/openils/lib/javascript/JSON_v1.js',

commit f6eeb168a73ac86506890997ae6f426d8ddaedbe
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Aug 4 17:56:32 2016 -0400

    LP#1522635 Webstaff lost (etc.) checkout completes
    
    Resolves an issue in the browser client where checkout of LOST
    (and other non status='checked out' copies) failed to show the
    open-circ-exists dialog.  In short, when searching for an existing
    open circulation, look for any open circulation linked to the copy,
    regardless of the status of the copy.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
index 7f953d6..753f54b 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
@@ -843,18 +843,12 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         if (angular.isArray(evt)) evt = evt[0];
 
         if (!evt.payload.old_circ) {
-            return egCore.net.request(
-                'open-ils.search',
-                'open-ils.search.asset.copy.fleshed2.find_by_barcode',
-                params.copy_barcode
-            ).then(function(resp){
-                console.log(resp);
-                if (egCore.evt.parse(resp)) {
-                    console.error(egCore.evt.parse(resp));
-                } else {
-                   evt.payload.old_circ = resp.circulations()[0];
-                   return service.circ_exists_dialog_impl( evt, params, options );
-                }
+            return egCore.pcrud.search('circ',
+                {target_copy : evt.payload.copy.id(), checkin_time : null},
+                {limit : 1} // should only ever be 1
+            ).then(function(old_circ) {
+                evt.payload.old_circ = old_circ;
+               return service.circ_exists_dialog_impl(evt, params, options);
             });
         } else {
             return service.circ_exists_dialog_impl( evt, params, options );

commit 997a30a6973b9161c7336a086534c41810665f50
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Aug 4 15:22:22 2016 -0400

    LP#1464350 Webstaff home page catalog search
    
    Search the catalog directly from the home/splash page in the browser
    client.
    
    A side effect of this commit is that it's now possible to jump directly
    to a set of search results in the embedded catalog by going to:
    
    /eg/staff/cat/catalog/results?<query-params>
    
    E.g.
    
    /eg/staff/cat/catalog/results?query=scores&qtype=subject
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/t_splash.tt2 b/Open-ILS/src/templates/staff/t_splash.tt2
index 1ca0ea2..2f2eb5d 100644
--- a/Open-ILS/src/templates/staff/t_splash.tt2
+++ b/Open-ILS/src/templates/staff/t_splash.tt2
@@ -35,6 +35,19 @@
           <div class="panel-title text-center">[% l('Item Search and Cataloging') %]</div>
         </div>
         <div class="panel-body">
+          <div class="row">
+            <div class="col-md-8">
+                <input focus-me="focus_search" 
+                    class="form-control" ng-model="cat_query" type="text" 
+                    ng-keypress="catalog_search($event)"
+                    placeholder="[% l('Search catalog for...') %]"/>
+            </div>
+            <div class="col-md-4">
+                <button class='btn btn-default' ng-click="catalog_search()">
+                    [% l('Search') %]
+                </button>
+            </div>
+          </div>
           <div>
             <img src="/xul/server/skin/media/images/portal/bucket.png"/>
             <a target="_self" href="./cat/bucket/record/">[% l('Record Buckets') %]</a>
diff --git a/Open-ILS/web/js/ui/default/staff/app.js b/Open-ILS/web/js/ui/default/staff/app.js
index 5c155a9..41910b9 100644
--- a/Open-ILS/web/js/ui/default/staff/app.js
+++ b/Open-ILS/web/js/ui/default/staff/app.js
@@ -117,9 +117,17 @@ function($routeProvider , $locationProvider) {
 /**
  * Splash page dynamic content.
  */
-.controller('SplashCtrl', ['$scope',
-    function($scope) {
-        console.log('SplashCtrl');
+.controller('SplashCtrl', ['$scope', '$window', function($scope, $window) {
+    console.log('SplashCtrl');
+    $scope.focus_search = true;
+
+    $scope.catalog_search = function($event) {
+        $scope.focus_search = true;
+        if (!$scope.cat_query) return;
+        if ($event && $event.keyCode != 13) return; // input ng-keypress
+        $window.location.href = 
+            '/eg/staff/cat/catalog/results?query=' + 
+            encodeURIComponent($scope.cat_query);
     }
-]);
+}]);
 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 729469b..2272ae7 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -40,6 +40,14 @@ angular.module('egCatalogApp', ['ui.bootstrap','ngRoute','ngLocationUpdate','egC
         resolve : resolver
     });
 
+    // Jump directly to the results page.  Any URL parameter 
+    // supported by the embedded catalog is supported here.
+    $routeProvider.when('/cat/catalog/results', {
+        templateUrl: './cat/catalog/t_catalog',
+        controller: 'CatalogCtrl',
+        resolve : resolver
+    });
+
     $routeProvider.when('/cat/catalog/retrieve_by_id', {
         templateUrl: './cat/catalog/t_retrieve_by_id',
         controller: 'CatalogRecordRetrieve',
@@ -1419,6 +1427,19 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
             url = url.replace(/advanced/, '/record/' + $scope.record_id);
         }
 
+        // Jumping directly to the results page by passing a search
+        // query via the URL.  Copy all URL params to the iframe url.
+        if ($location.path().match(/catalog\/results/)) {
+            url = url.replace(/advanced/, '/results?');
+            var first = true;
+            angular.forEach($location.search(), function(val, key) {
+                if (!first) url += '&';
+                first = false;
+                url += encodeURIComponent(key) 
+                    + '=' + encodeURIComponent(val);
+            });
+        }
+
         $scope.catalog_url = url;
     }
 

commit d78e8a4f5e3a9fa501573e7f26bf69de566632ad
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Wed Aug 10 12:10:58 2016 -0400

    start adding form validation to receipt template editor
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/css/style.css.tt2 b/Open-ILS/src/templates/staff/css/style.css.tt2
index fb06c0e..93881b6 100644
--- a/Open-ILS/src/templates/staff/css/style.css.tt2
+++ b/Open-ILS/src/templates/staff/css/style.css.tt2
@@ -97,8 +97,11 @@
 .form-validated input.ng-valid.ng-dirty {
   background-color: #78FA89;
 }
-.form-validated input.ng-invalid-required {
-  background-color: #FACDCF;
+.form-validated textarea.ng-invalid.ng-dirty {
+  background-color: #FA787E;
+}
+.form-validated textarea.ng-valid.ng-dirty {
+  background-color: #78FA89;
 }
 
 /* --------------------------------------------------------------------------
diff --git a/Open-ILS/src/templates/staff/reporter/t_edit_template.tt2 b/Open-ILS/src/templates/staff/reporter/t_edit_template.tt2
index cd8b74a..fbb266d 100644
--- a/Open-ILS/src/templates/staff/reporter/t_edit_template.tt2
+++ b/Open-ILS/src/templates/staff/reporter/t_edit_template.tt2
@@ -1,11 +1,12 @@
 <!-- report template builder -->
+<ng-form name="reportTemplateForm" class="form-validated">
 
 <div class="row">
   <div class="col-md-2">
     [% l('Template Name') %]
   </div>
   <div class="col-md-4">
-    <div><input type="text" class="form-control" ng-model="templateName"/></div>
+    <div><input type="text" class="form-control" ng-model="templateName" required/></div>
   </div>
   <div class="col-md-2">
     [% l('Documentation URL') %]
@@ -20,15 +21,16 @@
     [% l('Template Description') %]
   </div>
   <div class="col-md-10">
-    <div><textarea class="form-control" ng-model="templateDescription"/></div>
+    <div><textarea class="form-control" ng-model="templateDescription" required/></div>
   </div>
 </div>
 
 <div class="row">
   <div class="col-md-2">
-    <button ng-click="saveTemplate()" class="btn btn-default">[% l('Save Template') %]</button>
+    <button ng-disabled="reportTemplateForm.$invalid" ng-click="saveTemplate()" class="btn btn-default">[% l('Save Template') %]</button>
   </div>
 </div>
+</ng-form>
 
 <hr/>
 

commit b1f1f64b932bf113c7e6ddf80ee66b892dc6fdfd
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Aug 8 17:05:10 2016 -0400

    Ignore null fleshed objects in autofleshing grid columns
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js
index 96c7a83..6c3e075 100644
--- a/Open-ILS/web/js/ui/default/staff/services/grid.js
+++ b/Open-ILS/web/js/ui/default/staff/services/grid.js
@@ -1538,6 +1538,8 @@ angular.module('egGridMod',
                         }
                     }
 
+                    if (!obj) return '';
+
                     var cls = obj.classname;
                     if (cls && (class_obj = egCore.idl.classes[cls])) {
                         idl_field = class_obj.field_map[step];

commit 804a907f4b21132c2b5c7865eed955df2a5e3814
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Aug 8 15:09:43 2016 -0400

    Repair output popup, and add Reports to the splash page
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/t_splash.tt2 b/Open-ILS/src/templates/staff/t_splash.tt2
index 2884ba6..1ca0ea2 100644
--- a/Open-ILS/src/templates/staff/t_splash.tt2
+++ b/Open-ILS/src/templates/staff/t_splash.tt2
@@ -65,6 +65,12 @@
               [% l('Workstation Administration') %]
             </a>
           </div>
+          <div>
+            <img src="/xul/server/skin/media/images/portal/reports.png"/>
+            <a target="_top" href="./reporter/legacy/main">
+              [% l('Reports') %]
+            </a>
+          </div>
         </div>
       </div>
     </div>
diff --git a/Open-ILS/web/reports/oils_rpt_folder_window.js b/Open-ILS/web/reports/oils_rpt_folder_window.js
index 0fb78db..f5e0cf8 100644
--- a/Open-ILS/web/reports/oils_rpt_folder_window.js
+++ b/Open-ILS/web/reports/oils_rpt_folder_window.js
@@ -390,7 +390,7 @@ oilsRptFolderWindow.prototype.showOutput = function(sched) {
 		function(r) {
 			var url = oilsRptBuildOutputLink(r.template(), r.id(), sched.id());
 			_debug("launching report output view at URL: " + url);
-			if(isXUL()) 
+			if(isXUL() && !window.IAMBROWSER) 
 				xulG.new_tab(xulG.url_prefix('XUL_REMOTE_BROWSER?url=') + url,
 					{tab_name: dojo.string.substitute( rpt_strings.FOLDER_WINDOW_REPORT_OUTPUT, [r.name()] ), browser:false},
 					{no_xulG:false, show_nav_buttons:true, show_print_button:true});

commit 59af472f0726b7ffdc89c66dab685fd3e686c9f7
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Aug 8 14:15:09 2016 -0400

    Report Templates!
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 2420c0b..b21f7b3 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -9063,7 +9063,7 @@ SELECT  usr,
 			<link field="reports" reltype="has_many" key="folder" map="" class="rr"/>
 		</links>
 	</class>
-	<class id="rt" controller="open-ils.reporter-store" oils_obj:fieldmapper="reporter::template" oils_persist:tablename="reporter.template" reporter:label="Template">
+	<class id="rt" controller="open-ils.reporter-store open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="reporter::template" oils_persist:tablename="reporter.template" reporter:label="Template">
 		<fields oils_persist:primary="id" oils_persist:sequence="reporter.template_id_seq">
 			<field name="id" reporter:datatype="id" />
 			<field name="owner" reporter:datatype="link"/>
@@ -9079,6 +9079,14 @@ SELECT  usr,
 			<link field="folder" reltype="has_a" key="id" map="" class="rtf"/>
 			<link field="reports" reltype="has_many" key="template" map="" class="rr"/>
 		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create   permission="RUN_REPORTS" owning_user="owner" global_required="true"/>
+                <retrieve permission="RUN_REPORTS" owning_user="owner" global_required="true"/>
+                <update   permission="RUN_REPORTS" owning_user="owner" global_required="true"/>
+                <delete   permission="RUN_REPORTS" owning_user="owner" global_required="true"/>
+            </actions>
+        </permacrud>
 	</class>
 	<class id="rr" controller="open-ils.reporter-store" oils_obj:fieldmapper="reporter::report" oils_persist:tablename="reporter.report" reporter:label="Report">
 		<fields oils_persist:primary="id" oils_persist:sequence="reporter.report_id_seq">
@@ -10562,20 +10570,20 @@ SELECT  usr,
             -- -- If we expect to use pcrud to query against specific bibs, we probably want to do this.  However, if we're using this to populate a report, we
             -- -- may not.
             -- SELECT
-            --     bre.id AS "bib_id",
-            --     COALESCE( z.copy_count, 0 ) AS "copy_count",
-            --     COALESCE( z.hold_count, 0 ) AS "hold_count",
-            --     COALESCE( z.copy_hold_ratio, 0 ) AS "hold_copy_ratio"
+            --     bre.id AS bib_id,
+            --     COALESCE( z.copy_count, 0 ) AS copy_count,
+            --     COALESCE( z.hold_count, 0 ) AS hold_count,
+            --     COALESCE( z.copy_hold_ratio, 0 ) AS hold_copy_ratio
             -- FROM (
                 SELECT
-                    y.bre AS "id",
-                    COALESCE( x.copy_count, 0 ) AS "copy_count",
-                    y.hold_count AS "hold_count",
-                    (y.hold_count::REAL / (CASE WHEN x.copy_count = 0 OR x.copy_count IS NULL THEN 0.1 ELSE x.copy_count::REAL END)) AS "hold_copy_ratio"
+                    y.bre AS id,
+                    COALESCE( x.copy_count, 0 ) AS copy_count,
+                    y.hold_count AS hold_count,
+                    (y.hold_count::REAL / (CASE WHEN x.copy_count = 0 OR x.copy_count IS NULL THEN 0.1 ELSE x.copy_count::REAL END)) AS hold_copy_ratio
                 FROM (
                         SELECT
-                            (SELECT bib_record FROM reporter.hold_request_record r WHERE r.id = h.id LIMIT 1) AS "bre",
-                            COUNT(*) AS "hold_count"
+                            (SELECT bib_record FROM reporter.hold_request_record r WHERE r.id = h.id LIMIT 1) AS bre,
+                            COUNT(*) AS hold_count
                         FROM action.hold_request h
                         WHERE
                             cancel_time IS NULL
@@ -10587,8 +10595,8 @@ SELECT  usr,
                             (SELECT id
                                 FROM biblio.record_entry 
                                 WHERE id = (SELECT record FROM asset.call_number WHERE id = call_number and deleted is false)
-                            ) AS "bre", 
-                            COUNT(*) AS "copy_count"
+                            ) AS bre, 
+                            COUNT(*) AS copy_count
                         FROM asset.copy
                             JOIN asset.copy_location loc ON (copy.location = loc.id AND loc.holdable)
                         WHERE copy.holdable 
@@ -11527,7 +11535,7 @@ SELECT  usr,
 	</class>
 	<class id="hasholdscount" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action::has_holds_count" reporter:label="Copy Has Holds Count" oils_persist:readonly="true">
         <oils_persist:source_definition>
-	SELECT ahcm.target_copy AS "id",count(*) AS "count"
+	SELECT ahcm.target_copy AS id,count(*) AS count
 	 FROM
 	 action.hold_request ahr,
 	 action.hold_copy_map ahcm
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/IDL2js.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/IDL2js.pm
index 497b0db..a7486e4 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/IDL2js.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/IDL2js.pm
@@ -125,6 +125,11 @@ sub load_IDL {
         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
     }
 
+    $xml =~ s/<!--.*?-->//sg;     # filter out XML comments ...
+    $xml =~ s/(?:^|\s+)--.*$//mg; # and SQL comments ...
+    $xml =~ s/^\s+/ /mg;          # and extra leading spaces ...
+    $xml =~ s/\R*//g;             # and newlines
+
     my $output;
     try {
         my $idl_doc = XML::LibXML->load_xml(string => $xml);
diff --git a/Open-ILS/src/templates/staff/css/reporter.css b/Open-ILS/src/templates/staff/css/reporter.css
new file mode 100644
index 0000000..e69de29
diff --git a/Open-ILS/src/templates/staff/reporter/index.tt2 b/Open-ILS/src/templates/staff/reporter/index.tt2
new file mode 100644
index 0000000..ee6e2fc
--- /dev/null
+++ b/Open-ILS/src/templates/staff/reporter/index.tt2
@@ -0,0 +1,21 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Reporter"); 
+  ctx.page_app = "egReporter";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/dojo/opensrf/md5.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/eframe.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/reporter/services/template.js"></script>
+[% INCLUDE 'staff/reporter/share/report_strings.tt2' %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/reporter/template/app.js"></script>
+<link rel="stylesheet" href="[% ctx.base_path %]/staff/css/reporter.css" />
+[% END %]
+
+<div ng-view></div>
+
+[% END %]
diff --git a/Open-ILS/src/templates/staff/reporter/share/report_strings.tt2 b/Open-ILS/src/templates/staff/reporter/share/report_strings.tt2
new file mode 100644
index 0000000..a6d50cb
--- /dev/null
+++ b/Open-ILS/src/templates/staff/reporter/share/report_strings.tt2
@@ -0,0 +1,180 @@
+
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+
+s.RPT_BUILDER_CONFIRM_SAVE = '[% l( "Save Template?") %]';
+
+s.SELECT_TFORM = '[% l( "Select Transform") %]';
+s.SELECT_OP = '[% l( "Select Operator") %]';
+
+s.LINK_NULLABLE_DEFAULT = '[% l( "Default") %]';
+s.LINK_NULLABLE_RIGHT = '[% l( "Parent") %]';
+s.LINK_NULLABLE_LEFT = '[% l( "Child") %]';
+s.LINK_NULLABLE_NONE = '[% l( "None") %]';
+
+s.FILTERS_LABEL_EQUALS = '[% l("Equals") %]';
+s.FILTERS_LABEL_LIKE = '[% l( "Contains Matching substring") %]';
+s.FILTERS_LABEL_ILIKE = '[% l( "Contains Matching substring (ignore case)") %]';
+s.FILTERS_LABEL_GREATER_THAN = '[% l( "Greater than") %]';
+s.FILTERS_LABEL_GT_TIME = '[% l( "After (Date/Time)") %]';
+s.FILTERS_LABEL_GT_EQUAL = '[% l( "Greater than or equal to") %]';
+s.FILTERS_LABEL_GTE_TIME = '[% l( "On or After (Date/Time)") %]';
+s.FILTERS_LABEL_LESS_THAN = '[% l( "Less than") %]';
+s.FILTERS_LABEL_LT_TIME = '[% l( "Before (Date/Time)") %]';
+s.FILTERS_LABEL_LT_EQUAL = '[% l( "Less than or equal to") %]';
+s.FILTERS_LABEL_LSE_TIME = '[% l( "On or Before (Date/Time)") %]';
+s.FILTERS_LABEL_IN = '[% l( "In list") %]';
+s.FILTERS_LABEL_NOT_IN = '[% l( "Not in list") %]';
+s.FILTERS_LABEL_BETWEEN = '[% l( "Between") %]';
+s.FILTERS_LABEL_NOT_BETWEEN = '[% l( "Not between") %]';
+s.FILTERS_LABEL_NULL = '[% l( "Is NULL") %]';
+s.FILTERS_LABEL_NOT_NULL = '[% l( "Is not NULL") %]';
+s.FILTERS_LABEL_NULL_BLANK = '[% l( "Is NULL or Blank") %]';
+s.FILTERS_LABEL_NOT_NULL_BLANK = '[% l( "Is not NULL or Blank") %]';
+s.FILTERS_LABEL_EQ_ANY = '[% l( "Equals Any") %]';
+s.FILTERS_LABEL_NE_ANY = '[% l( "Does Not Equal Any") %]';
+
+s.FOLDERS_TEMPLATES = '[% l( "Templates") %]';
+s.FOLDERS_TEMPLATE = '[% l( "Template") %]';
+s.FOLDERS_REPORTS = '[% l( "Reports") %]';
+s.FOLDERS_REPORT = '[% l( "Report") %]';
+s.FOLDERS_OUTPUT = '[% l( "Output") %]';
+
+s.FOLDER_WINDOW_SELECT_ITEM = '[% l( "Please select an item from the list") %]';
+s.FOLDER_WINDOW_CHANGE_FOLDERS = '[% l( "Change Folders") %]';
+s.FOLDER_WINDOW_COLNAME_SELECT = '[% l( "Select") %]';
+s.FOLDER_WINDOW_COLNAME_ALL = '[% l( "All") %]';
+s.FOLDER_WINDOW_COLNAME_NONE = '[% l( "None") %]';
+
+s.REPORT_EDITOR_REPORT_FOLDERS = '[% l( "Report Folders") %]';
+s.REPORT_EDITOR_OUTPUT_FOLDERS = '[% l( "Output Folders") %]';
+s.REPORT_EDITOR_PROVIDE_FOLDER_ALERT = '[% l( "Please provide a report folder") %]';
+s.REPORT_EDITOR_ENTER_NAME_ALERT = '[% l( "Please enter a report name") %]';
+s.REPORT_EDITOR_ENTER_NEW_NAME_ALERT = '[% l( "Please change the report name") %]';
+s.REPORT_EDITOR_INVALID_DATE_ALERT = '[% l( "invalid start date -  YYYY-MM-DD") %]';
+s.REPORT_EDITOR_PROVIDE_OUTPUT_ALERT = '[% l( "Please provide an output folder") %]';
+
+s.TFORMS_LABEL_RAW_DATA = '[% l( "Raw Data") %]';
+s.TFORMS_LABEL_FIRST = '[% l( "First Value") %]';
+s.TFORMS_LABEL_LAST = '[% l( "Last Value") %]';
+s.TFORMS_LABEL_COUNT = '[% l( "Count") %]';
+s.TFORMS_LABEL_COUNT_DISTINCT = '[% l( "Count Distinct") %]';
+s.TFORMS_LABEL_MIN = '[% l( "Min") %]';
+s.TFORMS_LABEL_MAX = '[% l( "Max") %]';
+s.TFORMS_LABEL_LOWER = '[% l( "Lower case") %]';
+s.TFORMS_LABEL_UPPER = '[% l( "Upper case") %]';
+s.TFORMS_LABEL_FIRST5 = '[% l( "First 5 characters (for US ZIP code)") %]';
+s.TFORMS_LABEL_FIRST_WORD = '[% l( "First contiguous non-space string") %]';
+s.TFORMS_LABEL_DOW = '[% l( "Day of Week") %]';
+s.TFORMS_LABEL_DOM = '[% l( "Day of Month") %]';
+s.TFORMS_LABEL_DOY = '[% l( "Day of Year") %]';
+s.TFORMS_LABEL_WOY = '[% l( "Week of Year") %]';
+s.TFORMS_LABEL_MOY = '[% l( "Month of Year") %]';
+s.TFORMS_LABEL_QOY = '[% l( "Quarter of Year") %]';
+s.TFORMS_LABEL_HOD = '[% l( "Hour of day") %]';
+s.TFORMS_LABEL_DATE = '[% l( "Date") %]';
+s.TFORMS_LABEL_MONTH_TRUNC = '[% l( "Year + Month") %]';
+s.TFORMS_LABEL_YEAR_TRUNC = '[% l( "Year") %]';
+s.TFORMS_LABEL_HOUR_TRUNC = '[% l( "Hour") %]';
+s.TFORMS_LABEL_DAY_NAME = '[% l( "Day Name") %]';
+s.TFORMS_LABEL_MONTH_NAME = '[% l( "Month Name") %]';
+s.TFORMS_LABEL_AGE = '[% l( "Age") %]';
+s.TFORMS_LABEL_MONTHS_AGO = '[% l( "Months ago") %]';
+s.TFORMS_LABEL_QUARTERS_AGO = '[% l( "Quarters ago") %]';
+s.TFORMS_LABEL_SUM = '[% l( "Sum") %]';
+s.TFORMS_LABEL_AVERAGE = '[% l( "Average") %]';
+s.TFORMS_LABEL_ROUND = '[% l( "Round") %]';
+s.TFORMS_LABEL_INT = '[% l( "Drop trailing decimals") %]';
+
+s.WIDGET_DAYS = '[% l( "Day(s)") %]';
+s.WIDGET_MONTHS = '[% l( "Month(s)") %]';
+s.WIDGET_YEARS = '[% l( "Year(s)") %]';
+s.WIDGET_QUARTERS = '[% l( "Quarter(s)") %]';
+s.WIDGET_REAL_DATE = '[% l( "Real Date") %]';
+s.WIDGET_RELATIVE_DATE = '[% l( "Relative Date") %]';
+
+s.OPERATORS_EQUALS = '[% l( "Equals") %]';
+s.OPERATORS_LIKE = '[% l( "Contains Matching substring") %]';
+s.OPERATORS_ILIKE = '[% l( "Contains Matching substring (ignore case)") %]';
+s.OPERATORS_GREATER_THAN = '[% l( "Greater than") %]';
+s.OPERATORS_GT_TIME = '[% l( "After (Date/Time)") %]';
+s.OPERATORS_GT_EQUAL = '[% l( "Greater than or equal to") %]';
+s.OPERATORS_GTE_TIME = '[% l( "On or After (Date/Time)") %]';
+s.OPERATORS_LESS_THAN = '[% l( "Less than") %]';
+s.OPERATORS_LT_TIME = '[% l( "Before (Date/Time)") %]';
+s.OPERATORS_LT_EQUAL = '[% l( "Less than or equal to") %]';
+s.OPERATORS_LTE_TIME = '[% l( "On or Before (Date/Time)") %]';
+s.OPERATORS_IN_LIST = '[% l( "In list") %]';
+s.OPERATORS_NOT_IN_LIST = '[% l( "Not in list") %]';
+s.OPERATORS_BETWEEN = '[% l( "Between") %]';
+s.OPERATORS_NOT_BETWEEN = '[% l( "Not between") %]';
+s.OPERATORS_IS_NULL = '[% l( "Is NULL") %]';
+s.OPERATORS_IS_NOT_NULL = '[% l( "Is not NULL") %]';
+s.OPERATORS_NULL_BLANK = '[% l( "Is NULL or Blank") %]';
+s.OPERATORS_NOT_NULL_BLANK = '[% l( "Is not NULL or Blank") %]';
+s.OPERATORS_EQ_ANY = '[% l( "Equals Any") %]';
+s.OPERATORS_NE_ANY = '[% l( "Does Not Equal Any") %]';
+
+s.SOURCE_BROWSE_AGGREGATE = '[% l( "Aggregate") %]';
+s.SOURCE_BROWSE_NON_AGGREGATE = '[% l( "Non-Aggregate") %]';
+
+s.SOURCE_SETUP_CONFIRM_EXIT = '[% l( "You have started building a template! Selecting a new starting source will destroy the current template and start over.  Is this OK?") %]';
+s.SOURCE_SETUP_CORE_SOURCES = '[% l( "Core Sources") %]';
+s.SOURCE_SETUP_ALL_AVAIL_SOURCES = '[% l( "All Available Sources") %]';
+
+s.TEMPLATE_CONF_BARE = '[% l( "Bare") %]';
+s.TEMPLATE_CONF_RAW_DATA = '[% l( "Raw Data") %]';
+s.TEMPLATE_CONF_EQUALS = '[% l( "Equals") %]';
+s.TEMPLATE_CONF_CONFIRM_RESET = '[% l( "You have already added this field. Click OK if you would like to reset this field.") %]';
+s.TEMPLATE_CONF_PROMPT_CHANGE = '[% l( "Change the column header?") %]';
+s.TEMPLATE_FIELD_DOC_PROMPT_CHANGE = '[% l( "Change the field hint to:") %]';
+s.TEMPLATE_CONF_BOOLEAN_VALUE = '[% l( "Boolean Value") %]';
+s.TEMPLATE_CONF_SELECT_CANCEL = '[% l( "Select the value, or cancel:") %]';
+s.TEMPLATE_CONF_TRUE = '[% l( "True") %]';
+s.TEMPLATE_CONF_FALSE = '[% l( "False") %]';
+s.TEMPLATE_CONF_CONFIRM_STATE = '[% l( "Click OK for TRUE and Cancel for FALSE.") %]';
+s.TEMPLATE_CONF_NO_MATCH = '[% l( "Field does not match one of list (comma separated):") %]';
+s.TEMPLATE_CONF_NOT_BETWEEN = '[% l( "Field value is not between (comma separated):") %]';
+s.TEMPLATE_CONF_BETWEEN = '[% l( "Field value is between (comma separated):") %]';
+s.TEMPLATE_CONF_NOT_IN = '[% l( "Field does not match one of list (comma separated):") %]';
+s.TEMPLATE_CONF_IN = '[% l( "Field matches one of list (comma separated):") %]';
+s.TEMPLATE_CONF_DEFAULT = '[% l( "Value:") %]';
+s.TEMPLATE_CONF_CONFIRM_SAVE = '[% l( "Save Template?") %]';
+s.TEMPLATE_CONF_SUCCESS_SAVE = '[% l( "Template was successfully saved.") %]';
+s.TEMPLATE_CONF_FAIL_SAVE = '[% l( "Template save failed.") %]';
+
+s.TRANSFORMS_BARE = '[% l( "Raw Data") %]';
+s.TRANSFORMS_FIRST = '[% l( "First Value") %]';
+s.TRANSFORMS_LAST = '[% l( "Last Value") %]';
+s.TRANSFORMS_COUNT = '[% l( "Count") %]';
+s.TRANSFORMS_COUNT_DISTINCT = '[% l( "Count Distinct") %]';
+s.TRANSFORMS_MIN = '[% l( "Min") %]';
+s.TRANSFORMS_MAX = '[% l( "Max") %]';
+s.TRANSFORMS_SUBSTRING = '[% l( "Substring") %]';
+s.TRANSFORMS_LOWER = '[% l( "Lower case") %]';
+s.TRANSFORMS_UPPER = '[% l( "Upper case") %]';
+s.TRANSFORMS_FIRST5 = '[% l( "First 5 characters (for US ZIP code)") %]';
+s.TRANSFORMS_FIRST_WORD = '[% l( "First contiguous non-space string") %]';
+s.TRANSFORMS_DOW = '[% l( "Day of Week") %]';
+s.TRANSFORMS_DOM = '[% l( "Day of Month") %]';
+s.TRANSFORMS_DOY = '[% l( "Day of Year") %]';
+s.TRANSFORMS_WOY = '[% l( "Week of Year") %]';
+s.TRANSFORMS_MOY = '[% l( "Month of Year") %]';
+s.TRANSFORMS_QOY = '[% l( "Quarter of Year") %]';
+s.TRANSFORMS_HOD = '[% l( "Hour of day") %]';
+s.TRANSFORMS_DATE = '[% l( "Date") %]';
+s.TRANSFORMS_MONTH_TRUNC = '[% l( "Year + Month") %]';
+s.TRANSFORMS_YEAR_TRUNC = '[% l( "Year") %]';
+s.TRANSFORMS_HOUR_TRUNC = '[% l( "Hour") %]';
+s.TRANSFORMS_DAY_NAME = '[% l( "Day Name") %]';
+s.TRANSFORMS_MONTH_NAME = '[% l( "Month Name") %]';
+s.TRANSFORMS_AGE = '[% l( "Age") %]';
+s.TRANSFORMS_MONTHS_AGO = '[% l( "Months ago") %]';
+s.TRANSFORMS_QUARTERS_AGO = '[% l( "Quarters ago") %]';
+s.TRANSFORMS_SUM = '[% l( "Sum") %]';
+s.TRANSFORMS_AVERAGE = '[% l( "Average") %]';
+s.TRANSFORMS_ROUND = '[% l( "Round") %]';
+s.TRANSFORMS_INT = '[% l( "Drop trailing decimals") %]';
+
+}]);
+</script>
diff --git a/Open-ILS/src/templates/staff/reporter/t_edit_template.tt2 b/Open-ILS/src/templates/staff/reporter/t_edit_template.tt2
new file mode 100644
index 0000000..cd8b74a
--- /dev/null
+++ b/Open-ILS/src/templates/staff/reporter/t_edit_template.tt2
@@ -0,0 +1,233 @@
+<!-- report template builder -->
+
+<div class="row">
+  <div class="col-md-2">
+    [% l('Template Name') %]
+  </div>
+  <div class="col-md-4">
+    <div><input type="text" class="form-control" ng-model="templateName"/></div>
+  </div>
+  <div class="col-md-2">
+    [% l('Documentation URL') %]
+  </div>
+  <div class="col-md-4">
+    <div><input type="text" class="form-control" ng-model="templateDocURL"/></div>
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-md-2">
+    [% l('Template Description') %]
+  </div>
+  <div class="col-md-10">
+    <div><textarea class="form-control" ng-model="templateDescription"/></div>
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-md-2">
+    <button ng-click="saveTemplate()" class="btn btn-default">[% l('Save Template') %]</button>
+  </div>
+</div>
+
+<hr/>
+
+<div class="row panel" style="max-height: 400px; min-height: 400px;">
+  <div class="col-md-5" style="max-height: 400px; min-height: 400px; overflow-y: scroll;">
+
+    <div class="row">
+      <div class="col-xs-3"><strong>[% l('Core Source') %]</strong></div>
+      <div class="col-xs-6">
+        <div class="source-selector nullable">
+          <select class="form-control" ng-model="coreSource" ng-change="changeCoreSource()"
+            ng-options="s.name as s.label group by s.core_label for s in allSources">
+            <option value="">[% l('-- Select Source --') %]</option>
+          </select>
+        </div>
+      </div>
+      <div class="col-xs-3">
+        <label for="enable_nullability_cb">
+          [% l('Nullability') %]
+        </lable>
+        <input type="checkbox" ng-model="enable_nullability"/>
+      </div>
+    </div>
+
+    <br/>
+
+    <treecontrol
+        class="tree-light"
+        tree-model="class_tree"
+        on-node-toggle="treeExpand(node, expanded)"
+        on-selection="selectSource(node, selected, $path)"
+    >
+      <select
+          ng-show="enable_nullability"
+          ng-model="node.jtype"
+          ng-init="join_types = [{type:'inner',label:'[% l('Default') %]'},{type:'left',label:'[% l('Child nullable') %]'},{type:'right',label:'[% l('Parent nullable') %]'}]"
+          ng-options="j.type as j.label for j in join_types"></select>
+      {{ node.label || n.id }}
+    </treecontrol>
+
+  </div>
+  <div class="col-md-7">
+    <div class="row">
+      <div class="col-md-7" style="max-height: 400px; min-height: 400px; overflow-y: scroll;">
+        <div class="row">
+            <div class="col-xs-3"><strong>[% l('Source Path') %]</strong></div>
+            <div class="col-xs-9"><input type="text" class="form-control" ng-model="currentPathLabel"/></div>
+        </div>
+
+        <br/>
+
+        <treecontrol
+          class="tree-light"
+          tree-model="selected_source_fields"
+          selected-nodes="selected_source_field_list"
+          on-selection="selectFields()"
+          options="field_tree_opts"
+          filter-expression="filterFields"
+        >
+          <span ng-switch="" on="node.datatype">
+            <span ng-switch-when="bool" class="glyphicon glyphicon-ok-sign" aria-hidden="true"></span>
+            <span ng-switch-when="float" class="glyphicon glyphicon-sound-5-1" aria-hidden="true"></span>
+            <span ng-switch-when="id" class="glyphicon glyphicon-barcode" aria-hidden="true"></span>
+            <span ng-switch-when="int" class="glyphicon glyphicon-scale" aria-hidden="true"></span>
+            <span ng-switch-when="interval" class="glyphicon glyphicon-resize-horizontal" aria-hidden="true"></span>
+            <span ng-switch-when="link" class="glyphicon glyphicon-link" aria-hidden="true"></span>
+            <span ng-switch-when="money" class="glyphicon glyphicon-usd" aria-hidden="true"></span>
+            <span ng-switch-when="number" class="glyphicon glyphicon-scale" aria-hidden="true"></span>
+            <span ng-switch-when="org_unit" class="glyphicon glyphicon-tree-conifer" aria-hidden="true"></span>
+            <span ng-switch-when="text" class="glyphicon glyphicon-font" aria-hidden="true"></span>
+            <span ng-switch-when="timestamp" class="glyphicon glyphicon-calendar" aria-hidden="true"></span>
+          </span>
+          {{ node.label || node.name }}
+        </treecontrol>
+      </div>
+      <div class="col-md-5" style="max-height: 400px; min-height: 400px; overflow-y: scroll;">
+        <strong>[% l('Transform') %]</strong>
+        <br/>
+        <br/>
+        <br/>
+        <treecontrol
+          class="tree-light"
+          tree-model="available_field_transforms"
+          selected-node="selected_transform"
+          options="field_transforms_tree_opts"
+        >
+          {{ node.label || node.transform }}
+        </treecontrol>
+      </div>
+    </div>
+      </div>
+
+    <hr/>
+
+    <div class="row">
+      <div class="col-md-12">
+
+      <uib-tabset> 
+        <uib-tab index="0" heading="[% l('Display Fields') %]">
+          <eg-grid
+            id-field="index"
+            features="-sort,-multisort,-multiselect"
+            items-provider="grid_display_fields_provider"
+            grid-controls="display_grid_controls"
+          >
+            <eg-grid-action 
+              handler="changeDisplayLabel"
+              label="[% l('Change Column Label') %]">
+            </eg-grid-action>
+
+            <eg-grid-action 
+              handler="changeDisplayFieldDoc"
+              label="[% l('Change Column Documentation') %]">
+            </eg-grid-action>
+          
+            <eg-grid-action 
+              handler="changeTransform"
+              label="[% l('Change Transform') %]">
+            </eg-grid-action>
+          
+            <eg-grid-action 
+              handler="moveDisplayFieldUp"
+              label="[% l('Move Field Up') %]">
+            </eg-grid-action>
+          
+            <eg-grid-action 
+              handler="moveDisplayFieldDown"
+              label="[% l('Move Field Down') %]">
+            </eg-grid-action>
+          
+            <eg-grid-action 
+              handler="removeDisplayField"
+              label="[% l('Remove Field') %]">
+            </eg-grid-action>
+          
+            <eg-grid-menu-item handler="addDisplayFields"
+              label="[% l('Add Fields') %]"></eg-grid-menu-item>
+          
+            <eg-grid-field path='path_label' label="[% l('Source Path') %]"></eg-grid-field>
+            <eg-grid-field path='name' label="[% l('Column') %]" hidden></eg-grid-field>
+            <eg-grid-field path='doc_text' label="[% l('Documentation') %]" hidden></eg-grid-field>
+            <eg-grid-field path='label' label="[% l('Column Label') %]"></eg-grid-field>
+            <eg-grid-field path='datatype' label="[% l('Data Type') %]"></eg-grid-field>
+            <eg-grid-field path='transform.label' label="[% l('Field Transform') %]"></eg-grid-field>
+          </eg-grid>
+        </uib-tab>
+
+        <uib-tab index="1" heading="[% l('Filters') %]">
+          <eg-grid
+            id-field="index"
+            features="-sort,-multisort,-multiselect"
+            items-provider="grid_filter_fields_provider"
+            grid-controls="filter_grid_controls"
+          >
+            <eg-grid-action 
+              handler="changeFilterFieldDoc"
+              label="[% l('Change Column Documentation') %]">
+            </eg-grid-action>
+
+            <eg-grid-action 
+              handler="changeTransform"
+              label="[% l('Change Transform') %]">
+            </eg-grid-action>
+
+            <eg-grid-action 
+              handler="changeOperator"
+              label="[% l('Change Operator') %]">
+            </eg-grid-action>
+          
+            <eg-grid-action 
+              handler="changeFilterValue"
+              label="[% l('Change Filter Value') %]">
+            </eg-grid-action>
+          
+            <eg-grid-action 
+              handler="removeFilterValue"
+              label="[% l('Remove Filter Value') %]">
+            </eg-grid-action>
+      
+            <eg-grid-action 
+              handler="removeFilterField"
+              label="[% l('Remove Field') %]">
+            </eg-grid-action>
+          
+            <eg-grid-menu-item handler="addFilterFields"
+              label="[% l('Add Fields') %]"></eg-grid-menu-item>
+          
+            <eg-grid-field path='path_label' label="[% l('Source Path') %]"></eg-grid-field>
+            <eg-grid-field path='label' label="[% l('Name') %]"></eg-grid-field>
+            <eg-grid-field path='name' label="[% l('Column') %]"></eg-grid-field>
+            <eg-grid-field path='datatype' label="[% l('Data Type') %]"></eg-grid-field>
+            <eg-grid-field path='operator.label' label="[% l('Operator') %]"></eg-grid-field>
+            <eg-grid-field path='transform.label' label="[% l('Field Transform') %]"></eg-grid-field>
+            <eg-grid-field path='value' label="[% l('Filter Value') %]"></eg-grid-field>
+          </eg-grid>
+        </uib-tab>
+      </uib-tabset>
+
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/templates/staff/reporter/t_legacy.tt2 b/Open-ILS/src/templates/staff/reporter/t_legacy.tt2
new file mode 100644
index 0000000..cbaa379
--- /dev/null
+++ b/Open-ILS/src/templates/staff/reporter/t_legacy.tt2
@@ -0,0 +1 @@
+<eg-embed-frame save-space="150" url="rurl"></eg-embed-frame>
diff --git a/Open-ILS/src/templates/staff/share/t_confirm_dialog.tt2 b/Open-ILS/src/templates/staff/share/t_confirm_dialog.tt2
index a9d6ac1..c701886 100644
--- a/Open-ILS/src/templates/staff/share/t_confirm_dialog.tt2
+++ b/Open-ILS/src/templates/staff/share/t_confirm_dialog.tt2
@@ -11,7 +11,7 @@
   <div class="modal-footer">
     [% dialog_footer %]
     <input type="submit" class="btn btn-primary" 
-      ng-click="ok()" value="{{ ok_button_label || '[% l('OK/Continue') %]'}}"/>
+      ng-click="ok()" value="{{ ok_button_label || '[% l("OK/Continue") %]'}}"/>
     <button class="btn btn-warning" 
       ng-click="cancel()">{{ cancel_button_label || "[% l('Cancel') %]"}}</button>
   </div>
diff --git a/Open-ILS/src/templates/staff/share/t_select_dialog.tt2 b/Open-ILS/src/templates/staff/share/t_select_dialog.tt2
new file mode 100644
index 0000000..37e0f39
--- /dev/null
+++ b/Open-ILS/src/templates/staff/share/t_select_dialog.tt2
@@ -0,0 +1,23 @@
+<!--
+  Generic confirmation dialog
+-->
+<div>
+  <div class="modal-header">
+    <button type="button" class="close" 
+      ng-click="cancel()" aria-hidden="true">×</button>
+    <h4 class="modal-title alert alert-info">{{message}}</h4> 
+  </div>
+  <div class="modal-body">
+    <div class="row">
+      <div class="col-md-12">
+        <eg-basic-combo-box allow-all="true" list="args.list" selected="args.value" focus-me="focus"></eg-basic-combo-box>
+      </div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    [% dialog_footer %]
+    <input type="submit" class="btn btn-primary" 
+      ng-click="ok()" value="[% l('OK/Continue') %]"/>
+    <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+  </div>
+</div>
diff --git a/Open-ILS/web/js/ui/default/staff/bower.json b/Open-ILS/web/js/ui/default/staff/bower.json
index 6fceb94..dcadeee 100644
--- a/Open-ILS/web/js/ui/default/staff/bower.json
+++ b/Open-ILS/web/js/ui/default/staff/bower.json
@@ -27,7 +27,7 @@
     "angular-file-saver": "~1.1.0",
     "angular-location-update": "~0.0.2",
     "ngtoast": "~2.0.0",
-    "angular-tree-control": "~0.2.23",
+    "angular-tree-control": "~0.2.28",
     "angular-animate": "~1.5.3",
     "angular-hotkeys": "cfp-angular-hotkeys#^1.7.0"
   },
diff --git a/Open-ILS/web/js/ui/default/staff/reporter/services/template.js b/Open-ILS/web/js/ui/default/staff/reporter/services/template.js
new file mode 100644
index 0000000..1c3b4e0
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/reporter/services/template.js
@@ -0,0 +1,440 @@
+/**
+ * Report templates
+ */
+
+angular.module('egReportMod', ['egCoreMod', 'ui.bootstrap'])
+.factory('egReportTemplateSvc',
+
+       ['$uibModal','$q','egCore','egConfirmDialog','egAlertDialog',
+function($uibModal , $q , egCore , egConfirmDialog , egAlertDialog) {
+
+    //dojo.requireLocalization("openils.reports", "reports");
+    //var egCore.strings = dojo.i18n.getLocalization("openils.reports", "reports");
+
+    var OILS_RPT_DTYPE_ARRAY = 'array';
+    var OILS_RPT_DTYPE_STRING = 'text';
+    var OILS_RPT_DTYPE_MONEY = 'money';
+    var OILS_RPT_DTYPE_BOOL = 'bool';
+    var OILS_RPT_DTYPE_INT = 'int';
+    var OILS_RPT_DTYPE_ID = 'id';
+    var OILS_RPT_DTYPE_OU = 'org_unit';
+    var OILS_RPT_DTYPE_FLOAT = 'float';
+    var OILS_RPT_DTYPE_TIMESTAMP = 'timestamp';
+    var OILS_RPT_DTYPE_INTERVAL = 'interval';
+    var OILS_RPT_DTYPE_LINK = 'link';
+    var OILS_RPT_DTYPE_NONE = '';
+    var OILS_RPT_DTYPE_NULL = null;
+    var OILS_RPT_DTYPE_UNDEF;
+    
+    var OILS_RPT_DTYPE_ALL = [
+    	OILS_RPT_DTYPE_STRING,
+    	OILS_RPT_DTYPE_MONEY,
+    	OILS_RPT_DTYPE_INT,
+    	OILS_RPT_DTYPE_ID,
+    	OILS_RPT_DTYPE_FLOAT,
+    	OILS_RPT_DTYPE_TIMESTAMP,
+    	OILS_RPT_DTYPE_BOOL,
+    	OILS_RPT_DTYPE_OU,
+    	OILS_RPT_DTYPE_NONE,
+    	OILS_RPT_DTYPE_NULL,
+    	OILS_RPT_DTYPE_UNDEF,
+    	OILS_RPT_DTYPE_INTERVAL,
+    	OILS_RPT_DTYPE_LINK
+    ];
+    var OILS_RPT_DTYPE_NOT_ID = [OILS_RPT_DTYPE_STRING,OILS_RPT_DTYPE_MONEY,OILS_RPT_DTYPE_INT,OILS_RPT_DTYPE_FLOAT,OILS_RPT_DTYPE_TIMESTAMP];
+    var OILS_RPT_DTYPE_NOT_BOOL = [OILS_RPT_DTYPE_STRING,OILS_RPT_DTYPE_MONEY,OILS_RPT_DTYPE_INT,OILS_RPT_DTYPE_FLOAT,OILS_RPT_DTYPE_TIMESTAMP,OILS_RPT_DTYPE_ID,OILS_RPT_DTYPE_OU,OILS_RPT_DTYPE_LINK];
+
+    var service = {
+        display_fields : [],
+        filter_fields  : [],
+
+        Filters : {
+        	'=' : {
+        		label : egCore.strings.OPERATORS_EQUALS
+        	},
+        
+        	'like' : {
+        		label : egCore.strings.OPERATORS_LIKE
+        	}, 
+        
+        	ilike : {
+        		label : egCore.strings.OPERATORS_ILIKE
+        	},
+        
+        	'>' : {
+        		label : egCore.strings.OPERATORS_GREATER_THAN,
+        		labels : { timestamp : egCore.strings.OPERATORS_GT_TIME }
+        	},
+        
+        	'>=' : {
+        		label : egCore.strings.OPERATORS_GT_EQUAL,
+        		labels : { timestamp : egCore.strings.OPERATORS_GTE_TIME }
+        	}, 
+        
+        
+        	'<' : {
+        		label : egCore.strings.OPERATORS_LESS_THAN,
+        		labels : { timestamp : egCore.strings.OPERATORS_LT_TIME }
+        	}, 
+        
+        	'<=' : {
+        		label : egCore.strings.OPERATORS_LT_EQUAL, 
+        		labels : { timestamp : egCore.strings.OPERATORS_LTE_TIME }
+        	},
+        
+        	'in' : {
+        		label : egCore.strings.OPERATORS_IN_LIST
+        	},
+        
+        	'not in' : {
+        		label : egCore.strings.OPERATORS_NOT_IN_LIST
+        	},
+        
+        	'between' : {
+        		label : egCore.strings.OPERATORS_BETWEEN
+        	},
+        
+        	'not between' : {
+        		label : egCore.strings.OPERATORS_NOT_BETWEEN
+        	},
+        
+        	'is' : {
+        		label : egCore.strings.OPERATORS_IS_NULL
+        	},
+        
+        	'is not' : {
+        		label : egCore.strings.OPERATORS_IS_NOT_NULL
+        	},
+        
+        	'is blank' : {
+        		label : egCore.strings.OPERATORS_NULL_BLANK
+        	},
+        
+        	'is not blank' : {
+        		label : egCore.strings.OPERATORS_NOT_NULL_BLANK
+        	},
+        
+        	'= any' : {
+        		labels : { 'array' : egCore.strings.OPERATORS_EQ_ANY }
+        	},
+        
+        	'<> any' : {
+        		labels : { 'array' : egCore.strings.OPERATORS_NE_ANY }
+        	}
+        },
+
+        Transforms : {
+           Bare : {
+                datatype : OILS_RPT_DTYPE_ALL,
+                label : egCore.strings.TRANSFORMS_BARE
+            },
+        
+            first : {
+                datatype : OILS_RPT_DTYPE_NOT_ID,
+                label : egCore.strings.TRANSFORMS_FIRST
+            },
+        
+            last : {
+                datatype : OILS_RPT_DTYPE_NOT_ID,
+                label : egCore.strings.TRANSFORMS_LAST
+            },
+        
+            count : {
+                datatype : OILS_RPT_DTYPE_NOT_BOOL,
+                aggregate : true,
+                label :  egCore.strings.TRANSFORMS_COUNT
+            },
+        
+            count_distinct : {
+                datatype : OILS_RPT_DTYPE_NOT_BOOL,
+                aggregate : true,
+                label : egCore.strings.TRANSFORMS_COUNT_DISTINCT
+            },
+        
+            min : {
+                datatype : OILS_RPT_DTYPE_NOT_ID,
+                aggregate : true,
+                label : egCore.strings.TRANSFORMS_MIN
+            },
+        
+            max : {
+                datatype : OILS_RPT_DTYPE_NOT_ID,
+                aggregate : true,
+                label : egCore.strings.TRANSFORMS_MAX
+            },
+        
+            /* string transforms ------------------------- */
+        
+            substring : {
+                datatype : [ OILS_RPT_DTYPE_STRING ],
+                params : 2,
+                label : egCore.strings.TRANSFORMS_SUBSTRING
+            },
+        
+            lower : {
+                datatype : [ OILS_RPT_DTYPE_STRING ],
+                label : egCore.strings.TRANSFORMS_LOWER
+            },
+        
+            upper : {
+                datatype : [ OILS_RPT_DTYPE_STRING ],
+                label : egCore.strings.TRANSFORMS_UPPER
+            },
+        
+            first5 : {
+                datatype : [ OILS_RPT_DTYPE_STRING ],
+                label : egCore.strings.TRANSFORMS_FIRST5
+            },
+        
+                first_word : {
+                        datatype : [OILS_RPT_DTYPE_STRING, 'text'],
+                        label : egCore.strings.TRANSFORMS_FIRST_WORD
+                },
+        
+            /* timestamp transforms ----------------------- */
+            dow : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_DOW,
+                cal_format : '%w',
+                regex : /^[0-6]$/
+            },
+            dom : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_DOM,
+                cal_format : '%e',
+                regex : /^[0-9]{1,2}$/
+            },
+        
+            doy : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_DOY,
+                cal_format : '%j',
+                regex : /^[0-9]{1,3}$/
+            },
+        
+            woy : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_WOY,
+                cal_format : '%U',
+                regex : /^[0-9]{1,2}$/
+            },
+        
+            moy : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_MOY,
+                cal_format : '%m',
+                regex : /^\d{1,2}$/
+            },
+        
+            qoy : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_QOY,
+                regex : /^[1234]$/
+            }, 
+        
+            hod : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_HOD,
+                cal_format : '%H',
+                regex : /^\d{1,2}$/
+            }, 
+        
+            date : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_DATE,
+                regex : /^\d{4}-\d{2}-\d{2}$/,
+                hint  : 'YYYY-MM-DD',
+                cal_format : '%Y-%m-%d',
+                input_size : 10
+            },
+        
+            month_trunc : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_MONTH_TRUNC,
+                regex : /^\d{4}-\d{2}$/,
+                hint  : 'YYYY-MM',
+                cal_format : '%Y-%m',
+                input_size : 7
+            },
+        
+            year_trunc : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_YEAR_TRUNC,
+                regex : /^\d{4}$/,
+                hint  : 'YYYY',
+                cal_format : '%Y',
+                input_size : 4
+            },
+        
+            hour_trunc : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_HOUR_TRUNC,
+                regex : /^\d{2}$/,
+                hint  : 'HH',
+                cal_format : '%Y-%m-$d %H',
+                input_size : 2
+            },
+        
+            day_name : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                cal_format : '%A',
+                label : egCore.strings.TRANSFORMS_DAY_NAME
+            }, 
+        
+            month_name : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                cal_format : '%B',
+                label : egCore.strings.TRANSFORMS_MONTH_NAME
+            },
+            age : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_AGE
+            },
+        
+            months_ago : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_MONTHS_AGO
+            },
+        
+            quarters_ago : {
+                datatype : [ OILS_RPT_DTYPE_TIMESTAMP ],
+                label : egCore.strings.TRANSFORMS_QUARTERS_AGO
+            },
+        
+            /* int  / float transforms ----------------------------------- */
+            sum : {
+                datatype : [ OILS_RPT_DTYPE_INT, OILS_RPT_DTYPE_FLOAT, OILS_RPT_DTYPE_MONEY ],
+                label : egCore.strings.TRANSFORMS_SUM,
+                aggregate : true
+            }, 
+        
+            average : {
+                datatype : [ OILS_RPT_DTYPE_INT, OILS_RPT_DTYPE_FLOAT, OILS_RPT_DTYPE_MONEY ],
+                label : egCore.strings.TRANSFORMS_AVERAGE,
+                aggregate : true
+            },
+        
+            round : {
+                datatype : [ OILS_RPT_DTYPE_INT, OILS_RPT_DTYPE_FLOAT ],
+                label : egCore.strings.TRANSFORMS_ROUND,
+            },
+        
+            'int' : {
+                datatype : [ OILS_RPT_DTYPE_FLOAT ],
+                label : egCore.strings.TRANSFORMS_INT
+            }
+        }
+    };
+
+    service.addFields = function (type, fields, transform, source_label, source_path, op) {
+        fields.forEach(function(f) {
+            var l = f.label ? f.label : f.name;
+            var new_field = angular.extend(
+                {},
+                f,
+                { index : service[type].length,
+                  label : l,
+                  path  : source_path,
+                  path_label : source_label,
+                  operator   : op,
+                  transform  : transform,
+                  doc_text   : ''
+                }
+            );
+
+            var add = true;
+            service[type].forEach(function(e) {
+                if (e.name == new_field.name && e.path == new_field.path) add = false;
+            });
+            if (add) service[type].push(new_field);
+        });
+    }
+
+    service.moveFieldUp = function (type, field) {
+        var new_list = [];
+        while (service[type].length) {
+            var f = service[type].pop();
+            if (field.index == f.index && f.index > 0)
+                new_list.unshift(f,service[type].pop());
+            else
+                new_list.unshift(f);
+        }
+        new_list.forEach(function(f) {
+            service[type].push(angular.extend(f, { index : service[type].length}));
+        });
+    }
+
+    service.moveFieldDown = function (type, field) {
+        var new_list = [];
+        var start_len = service[type].length - 1;
+        while (service[type].length) {
+            var f = service[type].shift();
+            if (field.index == f.index && f.index < start_len)
+                new_list.push(service[type].shift(),f);
+            else
+                new_list.push(f);
+        }
+        new_list.forEach(function(f) {
+            service[type].push(angular.extend(f, { index : service[type].length}));
+        });
+    }
+
+    service.removeField = function (type, field) {
+        var new_list = [];
+        while (service[type].length) {
+            var f = service[type].pop();
+            if (field.index != f.index ) new_list.push(f);
+        }
+        new_list.forEach(function(f) {
+            service[type].push(angular.extend(f, { index : service[type].length}));
+        });
+    }
+
+    service.getTransformByLabel = function (l) {
+        for( var key in service.Transforms ) {
+            var t = service.Transforms[key];
+            if (l == t.label) return key;
+            if (angular.isArray(t.labels) && t.labels.indexOf(l) > -1) return key;
+        }
+        return null;
+    }
+
+    service.getFilterByLabel = function (l) {
+        for( var key in service.Filters ) {
+            var t = service.Filters[key];
+            if (l == t.label) return key;
+            if (angular.isArray(t.labels) && t.labels.indexOf(l) > -1) return key;
+        }
+        return null;
+    }
+
+    service.getTransforms = function (args) {
+        var dtype = args.datatype;
+        var agg = args.aggregate;
+        var nonagg = args.non_aggregate;
+        var label = args.label;
+    
+        var tforms = [];
+    
+        for( var key in service.Transforms ) {
+            var obj = service.Transforms[key];
+            if( agg && !nonagg && !obj.aggregate ) continue;
+            if( !agg && nonagg && obj.aggregate ) continue;
+            if( !dtype && obj.datatype.length > 0 ) continue;
+            if( dtype && obj.datatype.length > 0 && transformIsForDatatype(key,dtype).length == 0 ) continue;
+            tforms.push(key);
+        }
+    
+        return tforms;
+    }
+
+
+    service.transformIsForDatatype = function (tform, dtype) {
+        var obj = service.Transforms[tform];
+        return obj.datateype.filter(function(d) { return (d == dtype) })[0];
+    }
+
+    return service;
+}])
+;
+
diff --git a/Open-ILS/web/js/ui/default/staff/reporter/template/app.js b/Open-ILS/web/js/ui/default/staff/reporter/template/app.js
new file mode 100644
index 0000000..272a0eb
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/reporter/template/app.js
@@ -0,0 +1,600 @@
+/*
+ * Report template builder
+ */
+
+angular.module('egReporter',
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egReportMod', 'treeControl', 'ngToast'])
+
+.config(['ngToastProvider', function(ngToastProvider) {
+  ngToastProvider.configure({
+    verticalPosition: 'bottom',
+    animation: 'fade'
+  });
+}])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+    $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
+
+    var resolver = {delay : function(egStartup) {return egStartup.go()}};
+
+    $routeProvider.when('/reporter/template/clone/:folder/:id', {
+        templateUrl: './reporter/t_edit_template',
+        controller: 'ReporterTemplateEdit',
+        resolve : resolver
+    });
+
+    $routeProvider.when('/reporter/legacy/template/clone/:folder/:id', {
+        templateUrl: './reporter/t_legacy',
+        controller: 'ReporterTemplateLegacy',
+        resolve : resolver
+    });
+
+    $routeProvider.when('/reporter/template/new/:folder', {
+        templateUrl: './reporter/t_edit_template',
+        controller: 'ReporterTemplateEdit',
+        resolve : resolver
+    });
+
+    $routeProvider.when('/reporter/legacy/main', {
+        templateUrl: './reporter/t_legacy',
+        controller: 'ReporterTemplateLegacy',
+        resolve : resolver
+    });
+
+    // default page
+    $routeProvider.otherwise({redirectTo : '/reporter/legacy/main'});
+})
+
+/**
+ * controller for legacy template stuff
+ */
+.controller('ReporterTemplateLegacy',
+       ['$scope','$routeParams','$location','egCore',
+function($scope , $routeParams , $location , egCore) {
+
+    var template_id = $routeParams.id;
+    var folder_id = $routeParams.folder;
+
+    $scope.rurl = '/reports/oils_rpt.xhtml?ses=' + egCore.auth.token();
+
+    if (folder_id) {
+        $scope.rurl = '/reports/oils_rpt_builder.xhtml?ses=' +
+                        egCore.auth.token() + '&folder=' + folder_id;
+
+        if (template_id) $scope.rurl += '&ct=' + template_id;
+    }
+
+}])
+
+/**
+ * Uber-controller for template editing
+ */
+.controller('ReporterTemplateEdit',
+       ['$scope','$q','$routeParams','$location','$timeout','$window','egCore','$uibModal','egPromptDialog',
+        'egGridDataProvider','egReportTemplateSvc','$uibModal','egConfirmDialog','egSelectDialog','ngToast',
+function($scope , $q , $routeParams , $location , $timeout , $window,  egCore , $uibModal , egPromptDialog ,
+         egGridDataProvider , egReportTemplateSvc , $uibModal , egConfirmDialog , egSelectDialog , ngToast) {
+
+    function values(o) { return Object.keys(o).map(function(k){return o[k]}) };
+
+    var template_id = $routeParams.id;
+    var folder_id = $routeParams.folder;
+
+    $scope.grid_display_fields_provider = egGridDataProvider.instance({
+        get : function (offset, count) {
+            return this.arrayNotifier(egReportTemplateSvc.display_fields, offset, count);
+        }
+    });
+    $scope.grid_filter_fields_provider = egGridDataProvider.instance({
+        get : function (offset, count) {
+            return this.arrayNotifier(egReportTemplateSvc.filter_fields, offset, count);
+        }
+    });
+
+    var dgrid = $scope.display_grid_controls = {};
+    var fgrid = $scope.filter_grid_controls = {};
+
+    var default_filter_obj = {
+        op : '=',
+        label     : egReportTemplateSvc.Filters['='].label
+    };
+
+    var default_transform_obj = {
+        transform : 'Bare',
+        label     : egReportTemplateSvc.Transforms.Bare.label,
+        aggregate : false
+    };
+
+    function mergePaths (items) {
+        var tree = {};
+
+        items.forEach(function (item) {
+            var t = tree;
+            var join_path = '';
+
+            item.path.forEach(function (p, i, a) {
+                var alias; // unpredictable hashes are fine for intermediate tables
+
+                if (i) { // not at the top of the tree
+                    if (i == 1) join_path = join_path.split('-')[0];
+
+                    var uplink = p.uplink.name;
+                    join_path += '-' + uplink;
+                    alias = hex_md5(join_path);
+
+                    var uplink_alias = uplink + '-' + alias;
+
+                    if (!t.join) t.join = {};
+                    if (!t.join[uplink_alias]) t.join[uplink_alias] = {};
+
+                    t = t.join[uplink_alias];
+
+                    var djtype = 'inner';
+                    if (p.uplink.reltype != 'has_a') djtype = 'left';
+
+                    t.type = p.jtype || djtype;
+                    t.key = p.uplink.key;
+                } else {
+                    join_path = p.classname + '-' + p.classname;
+                    alias = hex_md5(join_path);
+                }
+
+                if (!t.alias) t.alias = alias;
+                t.path = join_path;
+
+                t.table = p.struct.source ? p.struct.source : p.table;
+                t.idlclass = p.classname;
+
+                if (a.length == i + 1) { // end of the path array, need a predictable hash
+                    t.label = item.path_label;
+                    t.alias = hex_md5(item.path_label);
+                }
+
+            });
+        });
+
+        return tree;
+    }
+
+    $scope.constructTemplate = function () {
+        var param_counter = 0;
+        return {
+            version     : 5,
+            doc_url     : $scope.templateDocURL,
+            core_class  : egCore.idl.classTree.top.classname,
+            select      : dgrid.allItems().map(function (i) {
+                            return {
+                                alias     : i.label,
+                                path      : i.path[i.path.length - 1].classname + '-' + i.name,
+                                field_doc : i.doc_text,
+                                relation  : hex_md5(i.path_label),
+                                column    : {
+                                    colname         : i.name,
+                                    transform       : i.transform ? i.transform.transform : '',
+                                    transform_label : i.transform ? i.transform.label : '',
+                                    aggregate       : !!i.transform.aggregate
+                                }
+                            }
+                          }),
+            from        : mergePaths( dgrid.allItems().concat(fgrid.allItems()) ),
+            where       : fgrid.allItems().filter(function(i) {
+                            return !i.transform.aggregate;
+                          }).map(function (i) {
+                            var cond = {};
+                            if (
+                                i.operator.op == 'is' ||
+                                i.operator.op == 'is not' ||
+                                i.operator.op == 'is blank' ||
+                                i.operator.op == 'is not blank'
+                            ) {
+                                cond[i.operator.op] = null;
+                            } else {
+                                if (i.value === undefined) {
+                                    cond[i.operator.op] = '::P' + param_counter++;
+                                }else {
+                                    cond[i.operator.op] = i.value;
+                                }
+                            }
+                            return {
+                                alias     : i.label,
+                                path      : i.path[i.path.length - 1].classname + '-' + i.name,
+                                field_doc : i.doc_text,
+                                relation  : hex_md5(i.path_label),
+                                column    : {
+                                    colname         : i.name,
+                                    transform       : i.transform.transform,
+                                    transform_label : i.transform.label,
+                                    aggregate       : 0
+                                },
+                                condition : cond // constructed above
+                            }
+                          }),
+            having      : fgrid.allItems().filter(function(i) {
+                            return !!i.transform.aggregate;
+                          }).map(function (i) {
+                            var cond = {};
+                            cond[i.operator.op] = '::P' + param_counter++;
+                            return {
+                                alias     : i.label,
+                                path      : i.path[i.path.length - 1].classname + '-' + i.name,
+                                field_doc : i.doc_text,
+                                relation  : hex_md5(i.path_label),
+                                column    : {
+                                    colname         : i.name,
+                                    transform       : i.transform.transform,
+                                    transform_label : i.transform.label,
+                                    aggregate       : 1
+                                },
+                                condition : cond // constructed above
+                            }
+                          }),
+            display_cols: angular.copy( dgrid.allItems() ).map(strip_item),
+            filter_cols : angular.copy( fgrid.allItems() ).map(strip_item)
+        };
+
+        function strip_item (i) {
+            delete i.children;
+            i.path.forEach(function(p){
+                delete p.children;
+                delete p.fields;
+                delete p.links;
+                delete p.struct.permacrud;
+                delete p.struct.field_map;
+                delete p.struct.fields;
+            });
+            return i;
+        }
+
+    }
+
+    function loadTemplate () {
+        if (!template_id) return;
+        egCore.pcrud.retrieve( 'rt', template_id)
+        .then( function(template) {
+            template.data = angular.fromJson(template.data());
+            if (template.data.version < 5) { // redirect to old editor...
+                $window.location.href = egCore.env.basePath + 'reporter/legacy/template/clone/'+folder_id + '/' + template_id;
+            // } else if (template.data.version < 5) { // redirect to old editor...
+            } else {
+                $scope.templateName = template.name() + ' (clone)';
+                $scope.templateDescription = template.description();
+                $scope.templateDocURL = template.data.doc_url;
+
+                $scope.changeCoreSource( template.data.core_class );
+
+                egReportTemplateSvc.display_fields = template.data.display_cols;
+                egReportTemplateSvc.filter_fields = template.data.filter_cols;
+
+                $timeout(function(){
+                    dgrid.refresh();
+                    fgrid.refresh();
+                });
+            }
+        });
+
+    }
+
+    $scope.saveTemplate = function () {
+        var tmpl = new egCore.idl.rt();
+        tmpl.name( $scope.templateName );
+        tmpl.description( $scope.templateDescription );
+        tmpl.owner(egCore.auth.user().id());
+        tmpl.folder(folder_id);
+        tmpl.data(angular.toJson($scope.constructTemplate()));
+
+        egConfirmDialog.open(tmpl.name(), egCore.strings.TEMPLATE_CONF_CONFIRM_SAVE,
+            {ok : function() {
+                return egCore.pcrud.create( tmpl )
+                .then(
+                    function() {
+                        ngToast.create(egCore.strings.TEMPLATE_CONF_SUCCESS_SAVE);
+                        return $timeout(
+                            function(){
+                                $window.location.href = egCore.env.basePath + 'reporter/legacy/main';
+                            },
+                            1000
+                        );
+                    },
+                    function() {
+                        ngToast.warning(egCore.strings.TEMPLATE_CONF_FAIL_SAVE);
+                    }
+                );
+            }}
+        );
+    }
+
+    $scope.addDisplayFields = function () {
+        var t = $scope.selected_transform;
+        if (!t) t = default_transform_obj;
+
+        egReportTemplateSvc.addFields(
+            'display_fields',
+            $scope.selected_source_field_list, 
+            t,
+            $scope.currentPathLabel,
+            $scope.currentPath
+        );
+        dgrid.refresh();
+    }
+
+    $scope.addFilterFields = function () {
+        var t = $scope.selected_transform;
+        if (!t) t = default_transform_obj;
+        f = default_filter_obj;
+
+        egReportTemplateSvc.addFields(
+            'filter_fields',
+            $scope.selected_source_field_list, 
+            t,
+            $scope.currentPathLabel,
+            $scope.currentPath,
+            f
+        );
+        fgrid.refresh();
+    }
+
+    $scope.moveDisplayFieldUp = function (items) {
+        items.reverse().forEach(function(item) {
+            egReportTemplateSvc.moveFieldUp('display_fields', item);
+        });
+        dgrid.refresh();
+    }
+
+    $scope.moveDisplayFieldDown = function (items) {
+        items.forEach(function(item) {
+            egReportTemplateSvc.moveFieldDown('display_fields', item);
+        });
+        dgrid.refresh();
+    }
+
+    $scope.removeDisplayField = function (items) {
+        items.forEach(function(item) {egReportTemplateSvc.removeField('display_fields', item)});
+        dgrid.refresh();
+    }
+
+    $scope.changeDisplayLabel = function (items) {
+        items.forEach(function(item) {
+            egPromptDialog.open(egCore.strings.TEMPLATE_CONF_PROMPT_CHANGE, item.label || '',
+                {ok : function(value) {
+                    if (value) egReportTemplateSvc.display_fields[item.index].label = value;
+                }}
+            );
+        });
+        dgrid.refresh();
+    }
+
+    $scope.changeDisplayFieldDoc = function (items) {
+        items.forEach(function(item) {
+            egPromptDialog.open(egCore.strings.TEMPLATE_FIELD_DOC_PROMPT_CHANGE, item.doc_text || '',
+                {ok : function(value) {
+                    if (value) egReportTemplateSvc.display_fields[item.index].doc_text = value;
+                }}
+            );
+        });
+        dgrid.refresh();
+    }
+
+    $scope.changeFilterFieldDoc = function (items) {
+        items.forEach(function(item) {
+            egPromptDialog.open(egCore.strings.TEMPLATE_FIELD_DOC_PROMPT_CHANGE, item.doc_text || '',
+                {ok : function(value) {
+                    if (value) egReportTemplateSvc.filter_fields[item.index].doc_text = value;
+                }}
+            );
+        });
+        fgrid.refresh();
+    }
+
+    $scope.changeFilterValue = function (items) {
+        items.forEach(function(item) {
+            var l = null;
+            egPromptDialog.open(egCore.strings.TEMPLATE_CONF_DEFAULT, item.value || '',
+                {ok : function(value) {
+                    if (value) egReportTemplateSvc.filter_fields[item.index].value = value;
+                }}
+            );
+        });
+        fgrid.refresh();
+    }
+
+    $scope.changeTransform = function (items) {
+
+        var f = items[0];
+
+        var tlist = [];
+        angular.forEach(egReportTemplateSvc.Transforms, function (o,n) {
+            if ( o.datatype.indexOf(f.datatype) > -1) {
+                if (tlist.indexOf(o.label) == -1) tlist.push( o.label );
+            }
+        });
+        
+        items.forEach(function(item) {
+            egSelectDialog.open(
+                egCore.strings.SELECT_TFORM, tlist, item.transform.label,
+                {ok : function(value) {
+                    if (value) {
+                        var t = egReportTemplateSvc.getTransformByLabel(value);
+                        item.transform = {
+                            label     : value,
+                            transform : t,
+                            aggregate : egReportTemplateSvc.Transforms[t].aggregate ? true : false
+                        };
+                    }
+                }}
+            );
+        });
+
+        fgrid.refresh();
+    }
+
+    $scope.changeOperator = function (items) {
+
+        var flist = [];
+        Object.keys(egReportTemplateSvc.Filters).forEach(function(k){
+            var v = egReportTemplateSvc.Filters[k];
+            if (flist.indexOf(v.label) == -1) flist.push(v.label);
+            if (v.labels && v.labels.length > 0) {
+                v.labels.forEach(function(l) {
+                    if (flist.indexOf(l) == -1) flist.push(l);
+                })
+            }
+        });
+
+        items.forEach(function(item) {
+            var l = item.operator ? item.operator.label : '';
+            egSelectDialog.open(
+                egCore.strings.SELECT_OP, flist, l,
+                {ok : function(value) {
+                    if (value) {
+                        var t = egReportTemplateSvc.getFilterByLabel(value);
+                        item.operator = { label: value, op : t };
+                    }
+                }}
+            );
+        });
+
+        fgrid.refresh();
+    }
+
+    $scope.removeFilterValue = function (items) {
+        items.forEach(function(item) {delete egReportTemplateSvc.filter_fields[item.index].value});
+        fgrid.refresh();
+    }
+
+    $scope.removeFilterField = function (items) {
+        items.forEach(function(item) {egReportTemplateSvc.removeField('filter_fields', item)});
+        fgrid.refresh();
+    }
+
+    $scope.allSources = values(egCore.idl.classes).sort( function(a,b) {
+        if (a.core && !b.core) return -1;
+        if (b.core && !a.core) return 1;
+        aname = a.label ? a.label : a.name;
+        bname = b.label ? b.label : b.name;
+        if (aname > bname) return 1;
+        return -1;
+    });
+
+    $scope.class_tree = [];
+    $scope.selected_source = null;
+    $scope.selected_source_fields = [];
+    $scope.selected_source_field_list = [];
+    $scope.available_field_transforms = [];
+    $scope.coreSource = null;
+    $scope.coreSourceChosen = false;
+    $scope.currentPathLabel = '';
+
+    $scope.treeExpand = function (node, expanding) {
+        if (expanding) node.children.map(egCore.idl.classTree.fleshNode);
+    }
+
+    $scope.filterFields = function (n) {
+        return n.virtual ? false : true;
+        // should we hide links?
+        return n.datatype && n.datatype != 'link'
+    }
+
+    $scope.field_tree_opts = {
+        multiSelection: true,
+        equality      : function(node1, node2) {
+            return node1.name == node2.name;
+        }
+    }
+
+    $scope.field_transforms_tree_opts = {
+        equality : function(node1, node2) {
+            if (!node2) return false;
+            return node1.transform == node2.transform;
+        }
+    }
+
+    $scope.selectFields = function () {
+        while ($scope.available_field_transforms.length) {
+            $scope.available_field_transforms.pop();
+        }
+
+        angular.forEach( $scope.selected_source_field_list, function (f) {
+            angular.forEach(egReportTemplateSvc.Transforms, function (o,n) {
+                if ( o.datatype.indexOf(f.datatype) > -1) {
+                    var include = true;
+
+                    angular.forEach($scope.available_field_transforms, function (t) {
+                        if (t.transform == n) include = false;
+                    });
+
+                    if (include) $scope.available_field_transforms.push({
+                        transform : n,
+                        label     : o.label,
+                        aggregate : o.aggregate ? true : false
+                    });
+                }
+            });
+        });
+
+    }
+
+    $scope.selectSource = function (node, selected, $path) {
+
+        while ($scope.selected_source_field_list.length) {
+            $scope.selected_source_field_list.pop();
+        }
+        while ($scope.selected_source_fields.length) {
+            $scope.selected_source_fields.pop();
+        }
+
+        if (selected) {
+            $scope.currentPath = angular.copy( $path().reverse() );
+            $scope.selected_source = node;
+            $scope.currentPathLabel = $scope.currentPath.map(function(n,i){
+                var l = n.label
+                if (i) l += ' (' + n.jtype + ')';
+                return l;
+            }).join( ' -> ' );
+            angular.forEach( node.fields, function (f) {
+                $scope.selected_source_fields.push( f );
+            });
+        } else {
+            $scope.currentPathLabel = '';
+        }
+
+        // console.log($scope.selected_source);
+    }
+
+    $scope.changeCoreSource = function (new_core) {
+        console.log('changeCoreSource: '+new_core);
+        function change_core () {
+            if (new_core) $scope.coreSource = new_core;
+            $scope.coreSourceChosen = true;
+
+            $scope.class_tree.pop();
+            $scope.class_tree.push(
+                egCore.idl.classTree.setTop($scope.coreSource)
+            );
+
+            while ($scope.selected_source_fields.length) {
+                $scope.selected_source_fields.pop();
+            }
+
+            while ($scope.available_field_transforms.length) {
+                $scope.available_field_transforms.pop();
+            }
+
+            $scope.currentPathLabel = '';
+        }
+
+        if ($scope.coreSourceChosen) {
+            egConfirmDialog.open(
+                egCore.strings.FOLDERS_TEMPLATE,
+                egCore.strings.SOURCE_SETUP_CONFIRM_EXIT,
+                {ok : change_core}
+            );
+        } else {
+            change_core();
+        }
+    }
+
+    loadTemplate();
+}])
+
+;
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 7e45256..3e41715 100644
--- a/Open-ILS/web/js/ui/default/staff/services/idl.js
+++ b/Open-ILS/web/js/ui/default/staff/services/idl.js
@@ -100,6 +100,9 @@ angular.module('egCoreMod')
          */
         function mkclass(cls, fields) {
 
+            service.classes[cls].core_label = service.classes[cls].core ? 'Core sources' : 'Non-core sources';
+            service.classes[cls].classname = cls;
+
             service[cls] = function(seed) {
                 this.a = seed || [];
                 this.classname = cls;
@@ -228,6 +231,81 @@ angular.module('egCoreMod')
         return hash;
     }
 
-    return service;
-}]);
+    // Using IDL links, allow construction of a tree-ish data structure from
+    // the IDL2js web service output.  This structure will be directly usable
+    // by the <treecontrol> directive
+    service.classTree = {
+        top : null
+    };
+
+    function _sort_class_fields (a,b) {
+        var aname = a.label || a.name;
+        var bname = b.label || b.name;
+        return aname > bname ? 1 : -1;
+    }
+
+    service.classTree.buildNode = function (cls, args) {
+        if (!cls) return null;
+
+        var n = service.classes[cls];
+        if (!n) return null;
+
+        if (!args)
+            args = { label : n.label };
+
+        args.id = cls;
+        if (args.from)
+            args.id = args.from + '.' + args.id;
+
+        return angular.extend( args, {
+            idl     : service[cls],
+            jtype   : 'inner',
+            uplink  : args.link,
+            classname: cls,
+            struct  : n,
+            table   : n.table,
+            fields  : n.fields.sort( _sort_class_fields ),
+            links   : n.fields
+                .filter( function(x) { return x.type == 'link'; } )
+                .sort( _sort_class_fields ),
+            children: []
+        });
+    }
+
+    service.classTree.fleshNode = function ( node ) {
+        if (node.children.length > 0)
+            return node; // already done already
 
+        angular.forEach(
+            node.links.sort( _sort_class_fields ),
+            function (n) {
+                var nlabel = n.label ? n.label : n.name;
+                node.children.push(
+                    service.classTree.buildNode(
+                        n["class"],
+                        {   label : nlabel,
+                            from  : node.id,
+                            link  : n
+                        }
+                    )
+                );
+            }
+        );
+
+        return node;
+    }
+
+    service.classTree.setTop = function (cls) {
+        console.debug('setTop: '+cls);
+        return service.classTree.top = service.classTree.fleshNode(
+            service.classTree.buildNode(cls)
+        );
+    }
+
+    service.classTree.getTop = function () {
+        return service.classTree.top;
+    }
+
+    return service;
+}])
+;
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 cbccf9a..6b4e325 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -185,6 +185,51 @@ function($uibModal, $interpolate) {
 }])
 
 /**
+ * egSelectDialog.open(
+ *    "message goes {{here}}", 
+ *    list,           // ['values','for','dropdown'],
+ *    selectedValue,  // optional
+ *    {
+ *      here : 'foo',
+ *      ok : function(value) {console.log(value)}, 
+ *      cancel : function() {console.log('prompt denied')}
+ *    }
+ *  );
+ */
+.factory('egSelectDialog', 
+    
+       ['$uibModal','$interpolate',
+function($uibModal, $interpolate) {
+    var service = {};
+
+    service.open = function(message, inputList, selectedValue, msg_scope) {
+        return $uibModal.open({
+            templateUrl: './share/t_select_dialog',
+            controller: ['$scope', '$uibModalInstance',
+                function($scope, $uibModalInstance) {
+                    $scope.message = $interpolate(message)(msg_scope);
+                    $scope.args = {
+                        list  : inputList,
+                        value : selectedValue
+                    };
+                    $scope.focus = true;
+                    $scope.ok = function() {
+                        if (msg_scope.ok) msg_scope.ok($scope.args.value);
+                        $uibModalInstance.close()
+                    }
+                    $scope.cancel = function() {
+                        if (msg_scope.cancel) msg_scope.cancel();
+                        $uibModalInstance.dismiss();
+                    }
+                }
+            ]
+        })
+    }
+
+    return service;
+}])
+
+/**
  * Warn on page unload and give the user a chance to avoid navigating
  * away from the current page.  
  * Only one handler is supported per page.
@@ -268,7 +313,8 @@ function($window , egStrings) {
         scope: {
             list: "=", // list of strings
             selected: "=",
-            egDisabled: "="
+            egDisabled: "=",
+            allowAll: "@",
         },
         template:
             '<div class="input-group">'+
@@ -277,33 +323,50 @@ function($window , egStrings) {
                     '<button type="button" ng-click="showAll()" class="btn btn-default dropdown-toggle"><span class="caret"></span></button>'+
                     '<ul class="dropdown-menu dropdown-menu-right">'+
                         '<li ng-repeat="item in list|filter:selected"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
-                        '<li ng-if="all" class="divider"><span></span></li>'+
-                        '<li ng-if="all" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
+                        '<li ng-if="complete_list" class="divider"><span></span></li>'+
+                        '<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
                     '</ul>'+
                 '</div>'+
             '</div>',
         controller: ['$scope','$filter',
             function( $scope , $filter) {
 
-                $scope.all = false;
+                $scope.complete_list = false;
                 $scope.isopen = false;
+                $scope.clickedopen = false;
+                $scope.clickedclosed = null;
 
                 $scope.showAll = function () {
-                    if ($scope.selected.length > 0)
-                        $scope.all = true;
+
+                    $scope.clickedopen = !$scope.clickedopen;
+
+                    if ($scope.clickedclosed === null) {
+                        if (!$scope.clickedopen) {
+                            $scope.clickedclosed = true;
+                        }
+                    } else {
+                        $scope.clickedclosed = !$scope.clickedopen;
+                    }
+
+                    if ($scope.selected.length > 0) $scope.complete_list = true;
+                    if ($scope.selected.length == 0) $scope.complete_list = false;
+                    $scope.makeOpen();
                 }
 
                 $scope.makeOpen = function () {
-                    $scope.isopen = $filter('filter')(
+                    $scope.isopen = $scope.clickedopen || ($filter('filter')(
                         $scope.list,
                         $scope.selected
-                    ).length > 0 && $scope.selected.length > 0;
-                    $scope.all = false;
+                    ).length > 0 && $scope.selected.length > 0);
+                    if ($scope.clickedclosed) $scope.isopen = false;
                 }
 
                 $scope.changeValue = function (newVal) {
                     $scope.selected = newVal;
                     $scope.isopen = false;
+                    $scope.clickedclosed = null;
+                    $scope.clickedopen = false;
+                    if ($scope.selected.length == 0) $scope.complete_list = false;
                 }
 
             }
diff --git a/Open-ILS/xsl/fm_IDL2js.xsl b/Open-ILS/xsl/fm_IDL2js.xsl
index da1c2b8..6c7709f 100644
--- a/Open-ILS/xsl/fm_IDL2js.xsl
+++ b/Open-ILS/xsl/fm_IDL2js.xsl
@@ -39,7 +39,7 @@ for (var c in _preload_fieldmapper_IDL) {
         </xsl:choose>
     </xsl:template>
  
-    <xsl:template match="idl:class"><xsl:value-of select="@id"/>:{name:"<xsl:value-of select="@id"/>",<xsl:if test="@reporter:label">label:"<xsl:value-of select="@reporter:label"/>",</xsl:if><xsl:if test="@oils_persist:restrict_primary">restrict_primary:"<xsl:value-of select="@oils_persist:restrict_primary"/>",</xsl:if><xsl:if test="@oils_persist:virtual = 'true'">virtual:true,</xsl:if><xsl:if test="idl:fields/@oils_persist:primary">pkey:"<xsl:value-of select="idl:fields/@oils_persist:primary"/>",</xsl:if><xsl:if test="idl:fields/@oils_persist:sequence">pkey_sequence:"<xsl:value-of select="idl:fields/@oils_persist:sequence"/>",</xsl:if><xsl:apply-templates select="idl:fields"/><xsl:apply-templates select="permacrud:permacrud"/>}</xsl:template>
+    <xsl:template match="idl:class"><xsl:value-of select="@id"/>:{name:"<xsl:value-of select="@id"/>",<xsl:if test="@reporter:label">label:"<xsl:value-of select="@reporter:label"/>",</xsl:if><xsl:if test="@oils_persist:restrict_primary">restrict_primary:"<xsl:value-of select="@oils_persist:restrict_primary"/>",</xsl:if><xsl:if test="@oils_persist:tablename">table:"<xsl:value-of select="@oils_persist:tablename"/>",</xsl:if><xsl:if test="@reporter:core = 'true'">core:true,</xsl:if><xsl:if test="@oils_persist:virtual = 'true'">virtual:true,</xsl:if><xsl:if test="oils_persist:source_definition">source:"(<xsl:value-of select="oils_persist:source_definition/text()"/>)",</xsl:if><xsl:if test="idl:fields/@oils_persist:primary">pkey:"<xsl:value-of select="idl:fields/@oils_persist:primary"/>",</xsl:if><xsl:if test="idl:fields/@oils_persist:sequence">pkey_sequence:"<xsl:value-of select="idl:fields/@oils_persist:sequence"/>",</xsl:if><xsl:apply-templates select="idl:fields"/><xsl:apply-
 templates select="permacrud:permacrud"/>}</xsl:template>
  
     <xsl:template match="idl:fields">fields:[<xsl:for-each select="idl:field"><xsl:call-template name="printField"><xsl:with-param name='pos' select="position()"/></xsl:call-template><xsl:if test="not(position() = last())">,</xsl:if></xsl:for-each>]</xsl:template>
 

commit 541921ab1de0a45002a501a2bea545c47f2c5252
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Aug 8 14:10:32 2016 -0400

    Let the legacy interface do the right thing when embedded in the web client
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/web/reports/oils_rpt_folder_window.js b/Open-ILS/web/reports/oils_rpt_folder_window.js
index 42a53d5..0fb78db 100644
--- a/Open-ILS/web/reports/oils_rpt_folder_window.js
+++ b/Open-ILS/web/reports/oils_rpt_folder_window.js
@@ -1,6 +1,9 @@
 dojo.requireLocalization("openils.reports", "reports");
 
 var rpt_strings = dojo.i18n.getLocalization("openils.reports", "reports");
+var NG_NEW_TEMPLATE_INTERFACE = '/eg/staff/reporter/template/new';
+var NG_CLONE_TEMPLATE_INTERFACE = '/eg/staff/reporter/template/clone';
+var NG_CLONE_LEGACY_TEMPLATE_INTERFACE = '/eg/staff/reporter/legacy/template/clone';
 var OILS_TEMPLATE_INTERFACE = 'xul/template_builder.xul';
 var OILS_LEGACY_TEMPLATE_INTERFACE = 'oils_rpt_builder.xhtml';
 
@@ -104,7 +107,11 @@ oilsRptFolderWindow.prototype.drawEditActions = function() {
 		var s = location.search+'';
 		s = s.replace(/\&folder=\d+/g,'');
 		s = s.replace(/\&ct=\d+/g,'');
-		goTo( OILS_TEMPLATE_INTERFACE+s+'&folder='+obj.folderNode.folder.id());
+		if (window.IAMBROWSER) {
+            window.top.location.href = NG_NEW_TEMPLATE_INTERFACE+'/'+obj.folderNode.folder.id();
+        } else {
+            goTo( OILS_TEMPLATE_INTERFACE+s+'&folder='+obj.folderNode.folder.id());
+        }
 	}
 
 
@@ -294,11 +301,21 @@ oilsRptFolderWindow.prototype.cloneTemplate = function(template) {
 			s = s.replace(/\&folder=\d+/g,'');
 			s = s.replace(/\&ct=\d+/g,'');
             version = JSON2js(template.data()).version;
-            if(version && version >= 2) {
-                _debug('entering new template building interface with template version ' + version);
-			    goTo(OILS_TEMPLATE_INTERFACE+s+'&folder='+folderid+'&ct='+template.id());
+            if(version && version >= 5) {
+			    window.top.location.href = NG_CLONE_TEMPLATE_INTERFACE+'/'+folderid+'/'+template.id();
+            } else if(version && version >= 2) {
+		        if (window.IAMBROWSER) {
+			        window.top.location.href = NG_CLONE_LEGACY_TEMPLATE_INTERFACE+'/'+folderid+'/'+template.id();
+                } else {
+                    _debug('entering new template building interface with template version ' + version);
+	    		    goTo(OILS_TEMPLATE_INTERFACE+s+'&folder='+folderid+'&ct='+template.id());
+                }
             } else {
-			    goTo(OILS_LEGACY_TEMPLATE_INTERFACE+s+'&folder='+folderid+'&ct='+template.id());
+		        if (window.IAMBROWSER) {
+			        window.top.location.href = NG_CLONE_LEGACY_TEMPLATE_INTERFACE+'/'+folderid+'/'+template.id();
+                } else {
+    			    goTo(OILS_LEGACY_TEMPLATE_INTERFACE+s+'&folder='+folderid+'&ct='+template.id());
+                }
             }
 		}
 	);
diff --git a/Open-ILS/web/reports/oils_rpt_param_editor.js b/Open-ILS/web/reports/oils_rpt_param_editor.js
index e66a986..e44a02d 100644
--- a/Open-ILS/web/reports/oils_rpt_param_editor.js
+++ b/Open-ILS/web/reports/oils_rpt_param_editor.js
@@ -38,7 +38,8 @@ oilsRptParamEditor.prototype.draw = function() {
 		var par = params[p];
 		var row = oilsRptParamEditor.row.cloneNode(true);
 		this.tbody.appendChild(row);
-		$n(row, 'column').appendChild(text(oilsRptMakeLabel(par.path)));
+		var clabel = oilsRptMakeLabel(par.path) || par.label || par.alias;
+		$n(row, 'column').appendChild(text(clabel));
 
         if (par.field_doc) {
 		    $n(row, 'column').appendChild(
diff --git a/Open-ILS/web/reports/oils_rpt_utils.js b/Open-ILS/web/reports/oils_rpt_utils.js
index d7017ec..2caed79 100644
--- a/Open-ILS/web/reports/oils_rpt_utils.js
+++ b/Open-ILS/web/reports/oils_rpt_utils.js
@@ -90,6 +90,9 @@ function oilsRptPathRel(path) {
 
 /* creates a label "path" based on the column path */
 function oilsRptMakeLabel(path) {
+    if (!path) return path;
+    if (path.indexOf(' ') > -1) return path
+
 	var parts = path.split(/-/);
 	var str = '';
 	for( var i = 0; i < parts.length; i++ ) {

commit 461eef4e0bd4caec45adafdce2ec9c8095a24256
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Mon Jul 11 15:57:48 2016 -0400

    toward ou type dropdown

diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
index ebb6e20..daba717 100644
--- a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
@@ -35,6 +35,18 @@
     </div>
     <div class="row">
         <div class="col-md-2">
+            <span>[% l('OU Type') %]</span>
+        </div>
+        <div class="col-md-9">
+            <select class="form-control" ng-model="selectedOUType">
+                <option ng-repeat="t in outypes" value="{{t}}" ng-selected="t == selectedOUType">
+                {{getOUTypeLabel(t)}}
+                </option>
+            </select>
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-2">
             <button class="form-control" ng-click="reset()">[% l('Reset Form') %]</button>
         </div>
         <div class="col-md-9">

commit d72697057908bbbecc961cc9a0eae421ab688c7d
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Mon Jul 11 14:52:53 2016 -0400

    child node creation
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
index 1d2e805..ebb6e20 100644
--- a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
@@ -60,6 +60,14 @@
             <input type="checkbox" ng-model="i_am_sure" ng-disabled="!org"/>
         </div>
     </div>
+    <div class="row">
+        <div class="col-md-2">
+            <button class="form-control" ng-click="new_child()">[% l('New Child') %]</button>
+        </div>
+        <div class="col-md-9">
+                [% l('NOTE: The new org unit will not exist in the database until Update Org is applied.') %]
+        </div>
+    </div>
 
 </form>
 
diff --git a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
index c5b34a7..f27f671 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
@@ -31,8 +31,8 @@ angular.module('egOrgUnitApp',
 })
 
 .controller('OrgUnitCtrl',
-       ['$scope','$q','$routeParams','$window','egCore','egOrg','ngToast',
-function($scope , $q , $routeParams , $window , egCore , egOrg , ngToast ) {
+       ['$scope','$q','$routeParams','$window','egCore','egIDL','egOrg','ngToast',
+function($scope , $q , $routeParams , $window , egCore , egIDL , egOrg , ngToast ) {
 
     $scope.reset = function() {
         $scope.org = angular.copy($scope.selectedNode);
@@ -92,12 +92,15 @@ function($scope , $q , $routeParams , $window , egCore , egOrg , ngToast ) {
     // main tab behavior
 
     $scope.update = function() {
-        var new_org = egOrg.get($scope.org.id);
+        var new_org = $scope.org.id == -1 ? new egIDL.aou() : egOrg.get($scope.org.id);
+        new_org.id( $scope.org.id );
+        new_org.parent_ou( $scope.org.parent_ou );
         new_org.name( $scope.org.name );
         new_org.shortname( $scope.org.shortname );
         new_org.email( $scope.org.email );
         new_org.phone( $scope.org.phone );
-        egCore.pcrud.update(new_org).then(
+        new_org.ou_type( 2 ); // FIXME
+        egCore.pcrud[$scope.org.id == -1 ? 'create' : 'update'](new_org).then(
             function(res) { // success
                 window.sessionStorage.removeItem('eg.env.aou.tree');
                 egCore.env.load();
@@ -129,5 +132,12 @@ function($scope , $q , $routeParams , $window , egCore , egOrg , ngToast ) {
         );
     };
 
+    $scope.new_child = function() {
+        $scope.org.parent_ou = $scope.org.id;
+        $scope.org.id = -1;
+        $scope.org.name = '';
+        $scope.org.shortname = '';
+    };
+
 }])
 

commit 60c2eb3b9137c7b07c1bdbb7c1cbd1ab598d0b5e
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Mon Jul 11 11:59:06 2016 -0400

    change to delete to remove, since JS uses delete
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
index 95c42b8..1d2e805 100644
--- a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
@@ -50,7 +50,7 @@
     </div>
     <div class="row">
         <div class="col-md-2">
-            <button class="form-control" ng-click="delete()" ng-disabled="!i_am_sure">[% l('Delete Org') %]</button>
+            <button class="form-control" ng-click="remove()" ng-disabled="!i_am_sure">[% l('Delete Org') %]</button>
         </div>
         <div class="col-md-9">
             <span>
diff --git a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
index ce358de..c5b34a7 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
@@ -112,7 +112,7 @@ function($scope , $q , $routeParams , $window , egCore , egOrg , ngToast ) {
         );
     };
 
-    $scope.delete = function() {
+    $scope.remove = function() {
         var new_org = egOrg.get($scope.org.id);
         egCore.pcrud.remove(new_org).then(
             function(res) { // success

commit a2d1ab854f916606d9af3ce991c9601ff8c9897f
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Mon Jul 11 11:25:00 2016 -0400

    UI tweak and org deletion
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/index.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/index.tt2
index 39a51ee..6cfe50b 100644
--- a/Open-ILS/src/templates/staff/admin/actor/org_unit/index.tt2
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/index.tt2
@@ -11,6 +11,8 @@
   angular.module('egCoreMod').run(['egStrings', function(s) {
     s.ORG_UPDATE_SUCCESS = "[% l('Org Unit Updated') %]";
     s.ORG_UPDATE_FAILURE = "[% l('Org Unit Not Updated') %]";
+    s.ORG_DELETE_SUCCESS = "[% l('Org Unit Deleted') %]";
+    s.ORG_DELETE_FAILURE = "[% l('Org Unit Not Deleted') %]";
   }])
 </script>
 <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/admin.css" />
diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
index 2343893..95c42b8 100644
--- a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
@@ -34,13 +34,33 @@
         </div>
     </div>
     <div class="row">
-        <div class="col-md-3">
+        <div class="col-md-2">
             <button class="form-control" ng-click="reset()">[% l('Reset Form') %]</button>
-            <button class="form-control" ng-click="update()" ng-disabled="orgForm.$invalid">[% l('Update Org') %]</button> [% l('NOTE: A server-side "autogen" process is needed to complete updates to the Org Hierarchy.') %]
         </div>
-        <div class="col-md-7">
+        <div class="col-md-9">
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-2">
+            <button class="form-control" ng-click="update()" ng-disabled="orgForm.$invalid">[% l('Update Org') %]</button>
+        </div>
+        <div class="col-md-9">
+            [% l('NOTE: A server-side "autogen" process is needed to complete updates, additions, and deletions to the Org Hierarchy.') %]
         </div>
     </div>
+    <div class="row">
+        <div class="col-md-2">
+            <button class="form-control" ng-click="delete()" ng-disabled="!i_am_sure">[% l('Delete Org') %]</button>
+        </div>
+        <div class="col-md-9">
+            <span>
+                [% l('NOTE: In practice, once an org unit has been used or assigned to items, patrons, etc. deletion becomes non-trivial and will require DBA intervention.') %]
+                [% l('Are you sure?') %]
+            </span>
+            <input type="checkbox" ng-model="i_am_sure" ng-disabled="!org"/>
+        </div>
+    </div>
+
 </form>
 
 </div>
diff --git a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
index 24b6226..ce358de 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
@@ -112,8 +112,22 @@ function($scope , $q , $routeParams , $window , egCore , egOrg , ngToast ) {
         );
     };
 
-
-
+    $scope.delete = function() {
+        var new_org = egOrg.get($scope.org.id);
+        egCore.pcrud.remove(new_org).then(
+            function(res) { // success
+                window.sessionStorage.removeItem('eg.env.aou.tree');
+                egCore.env.load();
+                init(0);
+                ngToast.create(egCore.strings.ORG_DELETE_SUCCESS);
+            },
+            function(res) { // failure
+                ngToast.create(egCore.strings.ORG_DELETE_FAILURE);
+            },
+            function(res) { // progress
+            }
+        );
+    };
 
 }])
 

commit 4e911171a113e5d7562ed998229f108a9120badc
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Mon Jul 11 10:20:43 2016 -0400

    toast, comments, logging
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/index.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/index.tt2
index 8959e19..39a51ee 100644
--- a/Open-ILS/src/templates/staff/admin/actor/org_unit/index.tt2
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/index.tt2
@@ -7,6 +7,12 @@
 [% BLOCK APP_JS %]
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/admin/actor/org_unit/app.js"></script>
+<script>
+  angular.module('egCoreMod').run(['egStrings', function(s) {
+    s.ORG_UPDATE_SUCCESS = "[% l('Org Unit Updated') %]";
+    s.ORG_UPDATE_FAILURE = "[% l('Org Unit Not Updated') %]";
+  }])
+</script>
 <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/admin.css" />
 [% END %]
 
diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
index 11134b7..2343893 100644
--- a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
@@ -36,7 +36,7 @@
     <div class="row">
         <div class="col-md-3">
             <button class="form-control" ng-click="reset()">[% l('Reset Form') %]</button>
-            <button class="form-control" ng-click="update()" ng-disabled="orgForm.$invalid">[% l('Update Org') %]</button>
+            <button class="form-control" ng-click="update()" ng-disabled="orgForm.$invalid">[% l('Update Org') %]</button> [% l('NOTE: A server-side "autogen" process is needed to complete updates to the Org Hierarchy.') %]
         </div>
         <div class="col-md-7">
         </div>
diff --git a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
index 6d2ebc8..24b6226 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
@@ -1,5 +1,12 @@
 angular.module('egOrgUnitApp',
-    ['ngRoute', 'ui.bootstrap', 'treeControl', 'egCoreMod', 'egUiMod'])
+    ['ngRoute', 'ui.bootstrap', 'treeControl', 'egCoreMod', 'egUiMod', 'ngToast'])
+
+.config(['ngToastProvider', function(ngToastProvider) {
+  ngToastProvider.configure({
+    verticalPosition: 'bottom',
+    animation: 'fade'
+  });
+}])
 
 .config(function($routeProvider, $locationProvider, $compileProvider) {
     $locationProvider.html5Mode(true);
@@ -24,8 +31,8 @@ angular.module('egOrgUnitApp',
 })
 
 .controller('OrgUnitCtrl',
-       ['$scope','$q','$routeParams','$window','egCore','egOrg',
-function($scope , $q , $routeParams , $window , egCore , egOrg  ) {
+       ['$scope','$q','$routeParams','$window','egCore','egOrg','ngToast',
+function($scope , $q , $routeParams , $window , egCore , egOrg , ngToast ) {
 
     $scope.reset = function() {
         $scope.org = angular.copy($scope.selectedNode);
@@ -92,19 +99,15 @@ function($scope , $q , $routeParams , $window , egCore , egOrg  ) {
         new_org.phone( $scope.org.phone );
         egCore.pcrud.update(new_org).then(
             function(res) { // success
-                console.log('handler1');
-                window.handler1 = res;
                 window.sessionStorage.removeItem('eg.env.aou.tree');
                 egCore.env.load();
                 init(0);
+                ngToast.create(egCore.strings.ORG_UPDATE_SUCCESS);
             },
-            function(res) { // success
-                console.log('handler2');
-                window.handler2 = res;
+            function(res) { // failure
+                ngToast.create(egCore.strings.ORG_UPDATE_FAILURE);
             },
-            function(res) { // error
-                console.log('handler3');
-                window.handler3 = res;
+            function(res) { // progress
             }
         );
     };

commit 872c8c2f0d98016fc1639dcc0d49300ae03e1660
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Fri Jun 10 14:22:45 2016 -0400

    programmatic selection of tree node still not working
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>

diff --git a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
index 323288e..6d2ebc8 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
@@ -35,9 +35,20 @@ function($scope , $q , $routeParams , $window , egCore , egOrg  ) {
 
     // the org tree
 
-    function init(n) {
+    function init(id) {
         $scope.treedata = [ egCore.idl.toHash( egOrg.tree() ) ];
-        $scope.selected = $scope.treedata[0]; // FIXME -- why no work?
+        function find_org(tree,id) {
+            if (tree.id==id) {
+                return tree;
+            }
+            for (var i in tree.children) {
+                var child = tree.children[i];
+                ou = find_org( child, id );
+                if (ou) { return ou; }
+            }
+            return null;
+        }
+        $scope.selected = find_org($scope.treedata,id) || $scope.treedata[0]; // FIXME -- why no work?
         $scope.expandedNodes = [ $scope.treedata[0], $scope.selected ];
     }
     init(1);

commit aca3dd932957a062eb5f6385bb89d3dfae08d011
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Fri Jun 10 14:17:13 2016 -0400

    refactor
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>

diff --git a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
index 6632bdc..323288e 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
@@ -27,31 +27,6 @@ angular.module('egOrgUnitApp',
        ['$scope','$q','$routeParams','$window','egCore','egOrg',
 function($scope , $q , $routeParams , $window , egCore , egOrg  ) {
 
-    $scope.update = function() {
-        var new_org = egOrg.get($scope.org.id);
-        new_org.name( $scope.org.name );
-        new_org.shortname( $scope.org.shortname );
-        new_org.email( $scope.org.email );
-        new_org.phone( $scope.org.phone );
-        egCore.pcrud.update(new_org).then(
-            function(res) { // success
-                console.log('handler1');
-                window.handler1 = res;
-                window.sessionStorage.removeItem('eg.env.aou.tree');
-                egCore.env.load();
-                init(0);
-            },
-            function(res) { // success
-                console.log('handler2');
-                window.handler2 = res;
-            },
-            function(res) { // error
-                console.log('handler3');
-                window.handler3 = res;
-            }
-        );
-    };
-
     $scope.reset = function() {
         $scope.org = angular.copy($scope.selectedNode);
     };
@@ -96,5 +71,35 @@ function($scope , $q , $routeParams , $window , egCore , egOrg  ) {
         }
     }
 
+    // main tab behavior
+
+    $scope.update = function() {
+        var new_org = egOrg.get($scope.org.id);
+        new_org.name( $scope.org.name );
+        new_org.shortname( $scope.org.shortname );
+        new_org.email( $scope.org.email );
+        new_org.phone( $scope.org.phone );
+        egCore.pcrud.update(new_org).then(
+            function(res) { // success
+                console.log('handler1');
+                window.handler1 = res;
+                window.sessionStorage.removeItem('eg.env.aou.tree');
+                egCore.env.load();
+                init(0);
+            },
+            function(res) { // success
+                console.log('handler2');
+                window.handler2 = res;
+            },
+            function(res) { // error
+                console.log('handler3');
+                window.handler3 = res;
+            }
+        );
+    };
+
+
+
+
 }])
 

commit d914aab30ba0ccf75a081aa8dfcf157e8385c1e1
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Fri Jun 10 14:16:22 2016 -0400

    getting changes to stick within a session
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>

diff --git a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
index bd24c80..6632bdc 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
@@ -37,6 +37,9 @@ function($scope , $q , $routeParams , $window , egCore , egOrg  ) {
             function(res) { // success
                 console.log('handler1');
                 window.handler1 = res;
+                window.sessionStorage.removeItem('eg.env.aou.tree');
+                egCore.env.load();
+                init(0);
             },
             function(res) { // success
                 console.log('handler2');
@@ -57,9 +60,18 @@ function($scope , $q , $routeParams , $window , egCore , egOrg  ) {
 
     // the org tree
 
-    $scope.treedata = [ egCore.idl.toHash( egOrg.tree() ) ];
-    $scope.selected = $scope.treedata[0]; // FIXME -- why no work?
-    $scope.expandedNodes = [ $scope.treedata[0] ];
+    function init(n) {
+        $scope.treedata = [ egCore.idl.toHash( egOrg.tree() ) ];
+        $scope.selected = $scope.treedata[0]; // FIXME -- why no work?
+        $scope.expandedNodes = [ $scope.treedata[0], $scope.selected ];
+    }
+    init(1);
+
+    window.phasefx = {
+         'scope' : $scope
+        ,'egorg' : egOrg
+        ,'egcore' : egCore
+    };
 
     $scope.showSelected = function(sel) {
         $scope.selectedNode = sel;

commit 297a0b142326320f54f83b1f3d9d7f7c9a0b495a
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Fri Jun 10 12:37:01 2016 -0400

    better stub out the other tabs, and try to select CONS by default
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_addresses_tab.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_addresses_tab.tt2
index e69de29..add988c 100644
--- a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_addresses_tab.tt2
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_addresses_tab.tt2
@@ -0,0 +1 @@
+<span>addresses</span>
diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_hours_tab.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_hours_tab.tt2
index e69de29..9a0c614 100644
--- a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_hours_tab.tt2
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_hours_tab.tt2
@@ -0,0 +1 @@
+<span>hours</span>
diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_index.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_index.tt2
index b45667f..1f883b4 100644
--- a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_index.tt2
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_index.tt2
@@ -39,10 +39,10 @@
                 [% INCLUDE 'staff/admin/actor/org_unit/t_main_tab.tt2' %]
             </div>
             <div ng-show="org_tab == 'hours'">
-                <span>hours tab</span>
+                [% INCLUDE 'staff/admin/actor/org_unit/t_hours_tab.tt2' %]
             </div>
             <div ng-show="org_tab == 'addresses'">
-                <span>addr tab</span>
+                [% INCLUDE 'staff/admin/actor/org_unit/t_addresses_tab.tt2' %]
             </div>
           </div>
         </div>
diff --git a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
index bab6a63..bd24c80 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
@@ -58,6 +58,7 @@ function($scope , $q , $routeParams , $window , egCore , egOrg  ) {
     // the org tree
 
     $scope.treedata = [ egCore.idl.toHash( egOrg.tree() ) ];
+    $scope.selected = $scope.treedata[0]; // FIXME -- why no work?
     $scope.expandedNodes = [ $scope.treedata[0] ];
 
     $scope.showSelected = function(sel) {

commit efa65bd5c7640758585070e66819f06e82522b78
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Wed May 11 15:17:47 2016 -0400

    Toward an Angular replacement for Org Units Conify
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/index.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/index.tt2
new file mode 100644
index 0000000..8959e19
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/index.tt2
@@ -0,0 +1,15 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Organizational Units");
+  ctx.page_app = "egOrgUnitApp";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/admin/actor/org_unit/app.js"></script>
+<link rel="stylesheet" href="[% ctx.base_path %]/staff/css/admin.css" />
+[% END %]
+
+<div ng-view></div>
+
+[% END %]
diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_addresses_tab.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_addresses_tab.tt2
new file mode 100644
index 0000000..e69de29
diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_hours_tab.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_hours_tab.tt2
new file mode 100644
index 0000000..e69de29
diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_index.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_index.tt2
new file mode 100644
index 0000000..b45667f
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_index.tt2
@@ -0,0 +1,51 @@
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    <span>[% l('Organizational Unit') %]</span>
+  </div>
+</div>
+
+<div class="row">
+    <div class="col-md-3">
+        <treecontrol class="tree-classic" tree-model="treedata" on-selection="showSelected(node)" expanded-nodes="expandedNodes">
+            {{node.shortname}} : {{node.name}} ({{node.children.length}})
+        </treecontrol>
+    </div>
+
+    <div class="col-md-9">
+
+        <span class="strong-text-1">{{selectedNode.name || ' '}}</span>
+
+        <ul class="nav nav-tabs">
+          <li ng-class="{active : org_tab == 'main'}">
+            <a ng-click="set_org_tab('main')" >
+                [% l('Main Settings') %]
+            </a>
+          </li>
+          <li ng-class="{active : org_tab == 'hours'}">
+            <a ng-click="set_org_tab('hours')" >
+                [% l('Hours of Operation') %]
+            </a>
+          </li>
+          <li ng-class="{active : org_tab == 'addresses'}">
+            <a ng-click="set_org_tab('addresses')" >
+                [% l('Addresses') %]
+            </a>
+          </li>
+        </ul>
+
+        <div class="tab-content">
+          <div class="tab-pane active">
+            <div ng-show="org_tab == 'main'">
+                [% INCLUDE 'staff/admin/actor/org_unit/t_main_tab.tt2' %]
+            </div>
+            <div ng-show="org_tab == 'hours'">
+                <span>hours tab</span>
+            </div>
+            <div ng-show="org_tab == 'addresses'">
+                <span>addr tab</span>
+            </div>
+          </div>
+        </div>
+
+    </div>
+</div>
diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
new file mode 100644
index 0000000..11134b7
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
@@ -0,0 +1,46 @@
+<div id="edit-org-container" class="container">
+
+<form name="orgForm" novalidate class="simple-form">
+    <div class="row">
+        <div class="col-md-2">
+            <span>[% l('Name') %]</span>
+        </div>
+        <div class="col-md-9">
+            <input class="form-control" type="text" ng-model="org.name" required ng-disabled="!org"/>
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-2">
+            <span>[% l('Short Name') %]</span>
+        </div>
+        <div class="col-md-9">
+            <input class="form-control" type="text" ng-model="org.shortname" required ng-disabled="!org"/>
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-2">
+            <span>[% l('Email') %]</span>
+        </div>
+        <div class="col-md-9">
+            <input class="form-control" type="text" ng-model="org.email" ng-disabled="!org"/>
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-2">
+            <span>[% l('Phone') %]</span>
+        </div>
+        <div class="col-md-9">
+            <input class="form-control" type="text" ng-model="org.phone" ng-disabled="!org"/>
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-3">
+            <button class="form-control" ng-click="reset()">[% l('Reset Form') %]</button>
+            <button class="form-control" ng-click="update()" ng-disabled="orgForm.$invalid">[% l('Update Org') %]</button>
+        </div>
+        <div class="col-md-7">
+        </div>
+    </div>
+</form>
+
+</div>
diff --git a/Open-ILS/src/templates/staff/css/admin.css.tt2 b/Open-ILS/src/templates/staff/css/admin.css.tt2
index 0de1df9..80e9ccf 100644
--- a/Open-ILS/src/templates/staff/css/admin.css.tt2
+++ b/Open-ILS/src/templates/staff/css/admin.css.tt2
@@ -28,3 +28,10 @@
 #auto-print-container .row {
   margin-top: 20px;
 }
+
+#edit-org-container input.ng-invalid-required {
+  background-color: yellow;
+  color: red;
+}
+
+
diff --git a/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
new file mode 100644
index 0000000..bab6a63
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
@@ -0,0 +1,87 @@
+angular.module('egOrgUnitApp',
+    ['ngRoute', 'ui.bootstrap', 'treeControl', 'egCoreMod', 'egUiMod'])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+    $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
+
+    var resolver = {delay :
+        ['egStartup', function(egStartup) {return egStartup.go()}]}
+
+    $routeProvider.when('/admin/actor/org_unit/:org_id', {
+        templateUrl: './admin/actor/org_unit/t_index',
+        controller: 'OrgUnitCtrl',
+        resolve : resolver
+    });
+
+    $routeProvider.when('/admin/actor/org_unit/', {
+        templateUrl: './admin/actor/org_unit/t_index',
+        controller: 'OrgUnitCtrl',
+        resolve : resolver
+    });
+
+    $routeProvider.otherwise({redirectTo : '/admin/actor/org_unit/'});
+})
+
+.controller('OrgUnitCtrl',
+       ['$scope','$q','$routeParams','$window','egCore','egOrg',
+function($scope , $q , $routeParams , $window , egCore , egOrg  ) {
+
+    $scope.update = function() {
+        var new_org = egOrg.get($scope.org.id);
+        new_org.name( $scope.org.name );
+        new_org.shortname( $scope.org.shortname );
+        new_org.email( $scope.org.email );
+        new_org.phone( $scope.org.phone );
+        egCore.pcrud.update(new_org).then(
+            function(res) { // success
+                console.log('handler1');
+                window.handler1 = res;
+            },
+            function(res) { // success
+                console.log('handler2');
+                window.handler2 = res;
+            },
+            function(res) { // error
+                console.log('handler3');
+                window.handler3 = res;
+            }
+        );
+    };
+
+    $scope.reset = function() {
+        $scope.org = angular.copy($scope.selectedNode);
+    };
+
+    $scope.reset();
+
+    // the org tree
+
+    $scope.treedata = [ egCore.idl.toHash( egOrg.tree() ) ];
+    $scope.expandedNodes = [ $scope.treedata[0] ];
+
+    $scope.showSelected = function(sel) {
+        $scope.selectedNode = sel;
+        $scope.org = angular.copy($scope.selectedNode);
+    };
+
+    // the tabs
+    $scope.org_tab = 'main';
+    $scope.set_org_tab = function(tab) {
+        $scope.org_tab = tab;
+
+        switch(tab) {
+
+            case 'main':
+                break;
+
+            case 'hours':
+                break;
+
+            case 'addresses':
+                break;
+        }
+    }
+
+}])
+

commit 4e93a8bd0d240a10ec545c927ca92864efba95e9
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Fri Jun 10 18:21:31 2016 -0400

    webstaff: add admin interface for MARC tag tables
    
    This is a simple interface using eg-edit-fm-record to start;
    more work will be required to better deal with the semantics
    for overriding tag definitions at various levels of the OU
    hierarchy.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/admin/server/config/marc_field.tt2 b/Open-ILS/src/templates/staff/admin/server/config/marc_field.tt2
new file mode 100644
index 0000000..3316ab6
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/server/config/marc_field.tt2
@@ -0,0 +1,53 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("MARC Tag Tables");
+  ctx.page_app = "egAdminConfig";
+  ctx.page_ctrl = 'MarcField';
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/fm_record_editor.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/admin/server/config/marc_field.js"></script>
+<link rel="stylesheet" href="[% ctx.base_path %]/staff/css/admin.css" />
+[% END %]
+
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    [% l('MARC Tag Tables') %]
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-md-2">
+    <div class="form-group">
+      <label>[% l('MARC Record Type') %]</label>
+      <select class="form-control" ng-model="marc_record_type">
+        <option value="biblio">[% l('Bibliographic') %]</option>
+        <option value="authority">[% l('Authority') %]</option>
+        <option value="serial">[% l('Holdings') %]</option>
+      </select>
+    </div>
+  </div>
+</div>
+
+<eg-grid
+    id-field="id"
+    idl-class="cmrcfld"
+    grid-controls="gridControls"
+    features="-multiselect"
+    persist-key="admin.server.config.marc_field">
+
+    <eg-grid-menu-item handler="new_record" label="[% l('New Record') %]"></eg-grid-menu-item>
+    <eg-grid-action handler="edit_record" label="[% l('Edit Record') %]"></eg-grid-action>
+    <eg-grid-action handler="delete_record" label="[% l('Delete Record') %]"></eg-grid-action>
+
+    <eg-grid-field label="[% l('Tag') %]"         path="tag"></eg-grid-field>
+    <eg-grid-field label="[% l('Name') %]"        path="name"></eg-grid-field>
+    <eg-grid-field label="[% l('Description') %]" path="description"></eg-grid-field>
+    <eg-grid-field label="[% l('ID') %]" path='id' required hidden></eg-grid-field>
+    <eg-grid-field path='*' hidden></eg-grid-field>
+</eg-grid>
+
+[% END %]
diff --git a/Open-ILS/src/templates/staff/admin/server/t_splash.tt2 b/Open-ILS/src/templates/staff/admin/server/t_splash.tt2
index 1431942..231dd5c 100644
--- a/Open-ILS/src/templates/staff/admin/server/t_splash.tt2
+++ b/Open-ILS/src/templates/staff/admin/server/t_splash.tt2
@@ -44,6 +44,7 @@
     ,[ l('MARC Search/Facet Classes'), "./admin/server/config/metabib_class" ]
     ,[ l('MARC Search/Facet Field FTS Maps'), "./admin/server/config/metabib_field_ts_map" ]
     ,[ l('MARC Search/Facet Fields'), "./admin/server/config/metabib_field" ]
+    ,[ l('MARC Tag Tables'), "./admin/server/config/marc_field" ]
     ,[ l('Org Unit Proximity Adjustments'), "./admin/server/config/org_unit_proximity_adjustment" ]
     ,[ l('Organization Types'), "./admin/server/legacy/actor/org_unit_type" ]
     ,[ l('Org Unit Setting Types'), "./admin/server/config/org_unit_setting_type" ]
@@ -59,7 +60,7 @@
     ,[ l('Z39.50 Servers'), "./admin/server/config/z3950_source" ]
    ];
 
-   USE table(interfaces, rows=16);
+   USE table(interfaces, rows=17);
 %]
 
 [% FOREACH row = table.rows %]
diff --git a/Open-ILS/web/js/ui/default/staff/admin/server/config/marc_field.js b/Open-ILS/web/js/ui/default/staff/admin/server/config/marc_field.js
new file mode 100644
index 0000000..fe6f93f
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/admin/server/config/marc_field.js
@@ -0,0 +1,88 @@
+angular.module('egAdminConfig',
+    ['ngRoute','ui.bootstrap','egCoreMod','egUiMod','egGridMod','egFmRecordEditorMod'])
+
+.controller('MarcField',
+       ['$scope','$q','$timeout','$location','$window','$uibModal','egCore','egGridDataProvider',
+        'egConfirmDialog',
+function($scope , $q , $timeout , $location , $window , $uibModal , egCore , egGridDataProvider ,
+         egConfirmDialog) {
+
+    egCore.startup.go(); // standalone mode requires manual startup
+
+    $scope.marc_record_type = 'biblio';
+    $scope.$watch('marc_record_type', function(newVal, oldVal) {
+        if (newVal != oldVal) {
+            $scope.gridControls.setQuery(generateQuery($scope.marc_record_type));
+            $scope.gridControls.refresh();
+        }
+    });
+
+    $scope.new_record = function() {
+        spawn_editor();
+    }
+
+    $scope.edit_record = function(items) {
+        if (items.length != 1) return;
+        spawn_editor(items[0].id);
+    }
+
+    spawn_editor = function(id) {
+        var templ;
+        if (arguments.length == 1) {
+            templ = '<eg-edit-fm-record idl-class="cmrcfld" mode="update" record-id="id" on-save="ok" on-cancel="cancel"></eg-edit-fm-record>';
+        } else {
+            templ = '<eg-edit-fm-record idl-class="cmrcfld" mode="create" on-save="ok" on-cancel="cancel"></eg-edit-fm-record>';
+        }
+        gridControls = $scope.gridControls;
+        $uibModal.open({
+            template : templ,
+            controller : [
+                        '$scope', '$uibModalInstance',
+                function($scope ,  $uibModalInstance) {
+                    $scope.id = id;
+
+                    $scope.ok = function($event) {
+                        $uibModalInstance.close();
+                        gridControls.refresh();
+                    }
+    
+                    $scope.cancel = function($event) {
+                        $uibModalInstance.dismiss();
+                    }
+                }
+            ]
+        });
+    }
+
+    $scope.delete_record = function(selected) {
+        if (!selected || !selected.length) return;
+
+        egCore.pcrud.retrieve('cmrcfld', selected[0].id).then(function(rec) {
+            egConfirmDialog.open(
+                egCore.strings.EG_CONFIRM_DELETE_RECORD_TITLE,
+                egCore.strings.EG_CONFIRM_DELETE_RECORD_BODY,
+                { id : rec.id() } // TODO replace with selector if available?
+            ).result.then(function() {
+                egCore.pcrud.remove(rec).then(function() {
+                    $scope.gridControls.refresh();
+                });
+            });
+        });
+    }
+
+    function generateQuery(marc_record_type) {
+        return {
+            'id' : { '!=' : null },
+            'marc_record_type' : marc_record_type
+        }
+    }
+
+    $scope.gridControls = {
+        setQuery : function() {
+            return generateQuery($scope.marc_record_type);
+        },
+        setSort : function() {
+            return ['tag'];
+        }
+    }
+}])

commit 3bf01639f186522674cd6d1f347b713f0d5766e8
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Fri Jun 10 18:19:17 2016 -0400

    webstaff: a couple strings for a generic record deletion dialog
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/base_js.tt2 b/Open-ILS/src/templates/staff/base_js.tt2
index c1b721b..bd3928f 100644
--- a/Open-ILS/src/templates/staff/base_js.tt2
+++ b/Open-ILS/src/templates/staff/base_js.tt2
@@ -74,6 +74,8 @@
     s.EG_WORK_LOG_FORGIVE_PAYMENT = '[% l('Forgive Payment') %]';
     s.EG_WORK_LOG_GOODS_PAYMENT = '[% l('Goods Payment') %]';
     s.EG_WORK_LOG_REQUESTED_HOLD = '[% l('Hold Request') %]';
+    s.EG_CONFIRM_DELETE_RECORD_TITLE = '[% l('Confirm Record Deletion') %]';
+    s.EG_CONFIRM_DELETE_RECORD_BODY = "[% l('Delete record {{id}}?') %]";
   }]);
 </script>
 

commit eabda938c992ae2269ec793eefd2ec0980f100ed
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Fri Jun 10 18:14:20 2016 -0400

    webstaff: new directive: egEditFmRecord
    
    This implements a generic IDL record editor widget:
    
    <eg-edit-fm-record
      idl-class            = "xyz"
      mode                 = "update"
      record-id            = "223"
      hidden-fields        = "bar,baz"
      readonly-fields      = "quux"
      required-fields      = "foo"
      is-required-override = "bundle_of_custom_functions"
      on-save              = "on_save_handler"
      on-cancel            = "on_cancel"
    ></eg-edit-fm-record>
    
    The mode can be either "create" or "update"; if it is "create",
    then it is not necessary or desired to pass a record-id.
    
    Currently eg-edit-fm-record expects to be invoked from
    inside a uibModal.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/share/t_fm_record_editor.tt2 b/Open-ILS/src/templates/staff/share/t_fm_record_editor.tt2
new file mode 100644
index 0000000..18ee809
--- /dev/null
+++ b/Open-ILS/src/templates/staff/share/t_fm_record_editor.tt2
@@ -0,0 +1,62 @@
+<form role="form" class="form-validated eg-edit-fm-record">
+
+  <div class="modal-header">
+    <button type="button" class="close"
+      ng-click="cancel()" aria-hidden="true">×</button>
+    <h4 class="modal-title">{{record_label}}</h4>
+  </div>
+  <div class="modal-body">
+    <div class="form-group row" ng-repeat="field in fields | filter:{virtual:'!true'}">
+      <div class="col-md-3">
+        <label for="rec-{{field.name}}">{{field.label}}</label>
+      </div>
+      <div class="col-md-9">
+        <span  ng-if="field.datatype == 'id'">{{rec[field.name]()}}</span>
+        <input ng-if="field.datatype == 'text'"
+          ng-readonly="field.readonly"
+          ng-required="field.is_required()"
+          ng-model="rec[field.name]"
+          ng-model-options="{ getterSetter : true }">
+        </input>
+        <input ng-if="field.datatype == 'int'"
+          type="number"
+          ng-readonly="field.readonly"
+          ng-required="field.is_required()"
+          ng-model="rec[field.name]"
+          ng-model-options="{ getterSetter : true }">
+        </input>
+        <input ng-if="field.datatype == 'float'"
+          type="number" step="0.1"
+          ng-readonly="field.readonly"
+          ng-required="field.is_required()"
+          ng-model="rec[field.name]"
+          ng-model-options="{ getterSetter : true }">
+        </input>
+        <input ng-if="field.datatype == 'bool'"
+          type="checkbox"
+          ng-readonly="field.readonly"
+          ng-model="rec[field.name]"
+          ng-model-options="{ getterSetter : true }">
+        </input>
+        <span ng-if="field.datatype == 'link'"
+          ng-class="{nullable : !field.is_required()}">
+          <select ng-if="field.datatype == 'link'"
+            ng-readonly="field.readonly"
+            ng-required="field.is_required()"
+            ng-options="item.id as item.name for item in field.linked_values"
+            ng-model="rec[field.name]"
+            ng-model-options="{ getterSetter : true }">
+          </select>
+        </span>
+        <eg-org-selector ng-if="field.datatype == 'org_unit'"
+          selected="rec_orgs[field.name]()"
+          onchange="rec_orgs[field.name]">
+        </eg-org-selector>
+      </div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button class="btn btn-primary" ng-click="ok()">[% l('Save') %]</button>
+    <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+  </div>
+</form>
diff --git a/Open-ILS/web/js/ui/default/staff/services/fm_record_editor.js b/Open-ILS/web/js/ui/default/staff/services/fm_record_editor.js
new file mode 100644
index 0000000..da95d54
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/services/fm_record_editor.js
@@ -0,0 +1,187 @@
+angular.module('egFmRecordEditorMod',
+    ['egCoreMod', 'egUiMod', 'ui.bootstrap'])
+
+.directive('egEditFmRecord', function() {
+    return {
+        restrict : 'AE',
+        transclude : true,
+        scope : {
+            // IDL class hint (e.g. "aou")
+            idlClass : '@',
+
+            // mode: 'create' for creating a new record,
+            //       'update' for editing an existing record
+            mode : '@',
+
+            // record ID to update
+            recordId : '=',
+
+            // comma-separated list of fields that should not be
+            // displayed
+            hiddenFields : '@',
+
+            // comma-separated list of fields that should always
+            // be read-only
+            readonlyFields : '@',
+
+            // comma-separated list of required fields; this
+            // supplements what the IDL considers required
+            requiredFields : '@',
+
+            // hash, keyed by field name, of functions to invoke
+            // to check whether a field is required.  Each
+            // callback is passed the field name and the record
+            // and should return a boolean value. This supports
+            // cases where whether a field is required or not
+            // depends on the current value of another field.
+            isRequiredOverride : '@',
+
+            // reference to handler to run upon saving
+            // record. The handler will be passed the
+            // record ID and a parameter indicating whether
+            // the save did a create or an update. Note that
+            // even if the mode of the egEditFmRecord is
+            // 'create', the onSave handler may still get
+            // 'update' if the user is permitted to create a
+            // record, then update it
+            onSave : '=',
+
+            // reference to handler to run if the user
+            // cancels the dialog
+            onCancel : '='
+
+        },
+
+        templateUrl : '/eg/staff/share/t_fm_record_editor',
+
+        controller : [
+                    '$scope','egCore',
+            function($scope , egCore) {
+
+            function list_to_hash(str) {
+                var hash = {};
+                if (angular.isString(str)) {
+                    str.split(/,/).map(function(s) {
+                        hash[s.trim()] = true;
+                    });
+                }
+                return hash;
+            }
+
+            $scope.required = list_to_hash($scope.requiredFields);
+            $scope.readonly = list_to_hash($scope.readonlyFields);
+            $scope.hidden = list_to_hash($scope.hiddenFields);
+
+            $scope.record_label = egCore.idl.classes[$scope.idlClass].label;
+            $scope.rec_orgs = {};
+
+            if ($scope.mode == 'update') {
+                egCore.pcrud.retrieve($scope.idlClass, $scope.recordId).then(function(r) {
+                    $scope.rec = r;
+                    convert_datatypes_to_js($scope.rec);
+                    $scope.fields = get_field_list();
+                });
+            } else {
+                $scope.rec = new egCore.idl[$scope.idlClass]();
+                $scope.fields = get_field_list();
+            }
+
+            function convert_datatypes_to_js(rec) {
+                var fields = egCore.idl.classes[$scope.idlClass].fields;
+                angular.forEach(fields, function(field) {
+                    if (field.datatype == 'bool') {
+                        if (rec[field.name]() == 't') {
+                            rec[field.name](true);
+                        } else if (rec[field.name]() == 'f') {
+                            rec[field.name](false);
+                        }
+                    }
+                });
+            }
+
+            function convert_datatypes_to_idl(rec) {
+                var fields = egCore.idl.classes[$scope.idlClass].fields;
+                angular.forEach(fields, function(field) {
+                    if (field.datatype == 'bool') {
+                        if (rec[field.name]() == true) {
+                            rec[field.name]('t');
+                        } else if (rec[field.name]() == false) {
+                            rec[field.name]('f');
+                        }
+                    }
+                });
+            }
+
+            function flatten_linked_values(cls, list) {
+                var results = [];
+                var fields = egCore.idl.classes[cls].fields;
+                var id_field;
+                var selector;
+                angular.forEach(fields, function(fld) {
+                    if (fld.datatype == 'id') {
+                        id_field = fld.name;
+                        selector = fld.selector ? fld.selector : id_field;
+                        return;
+                    }
+                });
+                angular.forEach(list, function(item) {
+                    var rec = egCore.idl.toHash(item);
+                    results.push({
+                        id : rec[id_field],
+                        name : rec[selector]
+                    });
+                });
+                return results;
+            }
+
+            function get_field_list() {
+                var fields = egCore.idl.classes[$scope.idlClass].fields;
+
+                angular.forEach(fields, function(field) {
+                    field.readonly = (field.name in $scope.readonly);
+                    if (angular.isObject($scope.isRequiredOverride) &&
+                        field.name in $scope.isRequiredOverride) {
+                        field.is_required = function() {
+                            return $scope.isRequiredOverride[field.name](field.name, $scope.rec);
+                        }
+                    } else {
+                        field.is_required = function() {
+                            return field.required || (field.name in $scope.required);
+                        }
+                    }
+                    if (field.datatype == 'link') {
+                    egCore.pcrud.retrieveAll(
+                            field.class, {}, {atomic : true}
+                        ).then(function(list) {
+                            field.linked_values = flatten_linked_values(field.class, list);
+                        });
+                    }
+                    if (field.datatype == 'org_unit') {
+                        $scope.rec_orgs[field.name] = function(org) {
+                            if (arguments.length == 1) $scope.rec[field.name](org.id());
+                            return egCore.org.get($scope.rec[field.name]());
+                        }
+                    }
+                });
+                return fields.filter(function(field) { return !(field.name in $scope.hidden) });
+            }
+
+            $scope.ok = function($event) {
+                var recToSave = egCore.idl.Clone($scope.rec)
+                convert_datatypes_to_idl(recToSave);
+                if ($scope.mode == 'update') {
+                    egCore.pcrud.update(recToSave).then(function() {
+                        $scope.onSave($event);
+                    });
+                } else {
+                    egCore.pcrud.create(recToSave).then(function() {
+                        $scope.onSave($event);
+                    });
+                }
+            }
+            $scope.cancel = function($event) {
+                $scope.onCancel($event);
+            }
+        }]
+    };
+})

commit d271b796cebc71e916e85249c15c2484444a390b
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Fri Jun 10 12:15:18 2016 -0400

    IDL improvements for classes releated to MARC tag tables
    
    In the course of building the admin interface for the
    MARC tag tables, some deficiencies in the IDL were
    run across. This patch fixes them.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index adda25f..2420c0b 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -880,10 +880,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 	</class>
 
 	<class id="cmrcfmt" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::marc_format" oils_persist:tablename="config.marc_format" reporter:label="MARC Formats" oils_persist:field_safe="true">
-		<fields oils_persist:primary="id">
-			<field reporter:label="ID"   name="id" reporter:datatype="id"/>
-			<field reporter:label="Code" name="code" reporter:datatype="text"/>
-			<field reporter:label="Name" name="name" reporter:datatype="text" oils_persist:i18n="true"/>
+		<fields oils_persist:primary="id" oils_persist:sequence="config.marc_format_id_seq">
+			<field reporter:label="ID"   name="id" reporter:datatype="id" reporter:selector="name" />
+			<field reporter:label="Code" name="code" reporter:datatype="text" oils_obj:required="true"/>
+			<field reporter:label="Name" name="name" reporter:datatype="text" oils_persist:i18n="true" oils_obj:required="true"/>
         </fields>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
@@ -896,18 +896,18 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 	</class>
 
 	<class id="cmrcfld" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::marc_field" oils_persist:tablename="config.marc_field" reporter:label="MARC Fields" oils_persist:field_safe="true">
-		<fields oils_persist:primary="id">
+		<fields oils_persist:primary="id" oils_persist:sequence="config.marc_field_id_seq">
 			<field reporter:label="ID"   name="id" reporter:datatype="id"/>
-			<field reporter:label="MARC Format" name="marc_format" reporter:datatype="link" />
-			<field reporter:label="MARC Record Type" name="marc_record_type" reporter:datatype="text" />
-			<field reporter:label="MARC Tag" name="tag" reporter:datatype="text" />
+			<field reporter:label="MARC Format" name="marc_format" reporter:datatype="link" oils_obj:required="true"/>
+			<field reporter:label="MARC Record Type" name="marc_record_type" reporter:datatype="text" oils_obj:required="true" />
+			<field reporter:label="MARC Tag" name="tag" reporter:datatype="text" oils_obj:required="true"/>
 			<field reporter:label="Name" name="name" reporter:datatype="text" oils_persist:i18n="true"/>
 			<field reporter:label="Description" name="description" reporter:datatype="text" oils_persist:i18n="true"/>
 			<field reporter:label="Fixed Field?" name="fixed_field" reporter:datatype="bool"/>
 			<field reporter:label="Repeatable?" name="repeatable" reporter:datatype="bool"/>
 			<field reporter:label="Mandatory?" name="mandatory" reporter:datatype="bool"/>
 			<field reporter:label="Hidden?" name="hidden" reporter:datatype="bool"/>
-			<field reporter:label="Owner" name="owner" reporter:datatype="link"/>
+			<field reporter:label="Owner" name="owner" reporter:datatype="org_unit"/>
         </fields>
 		<links>
 			<link field="marc_format" reltype="has_a" key="id" map="" class="cmrcfmt"/>
@@ -924,17 +924,17 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 	</class>
 
 	<class id="cmrcsubfld" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::marc_subfield" oils_persist:tablename="config.marc_subfield" reporter:label="MARC Subfields" oils_persist:subfield_safe="true">
-		<fields oils_persist:primary="id">
+		<fields oils_persist:primary="id" oils_persist:sequence="config.marc_subfield_id_seq">
 			<field reporter:label="ID"   name="id" reporter:datatype="id"/>
-			<field reporter:label="MARC Format" name="marc_format" reporter:datatype="link" />
-			<field reporter:label="MARC Record Type" name="marc_record_type" reporter:datatype="text" />
-			<field reporter:label="MARC Tag" name="tag" reporter:datatype="text" />
-			<field reporter:label="MARC Subfield" name="code" reporter:datatype="text" />
+			<field reporter:label="MARC Format" name="marc_format" reporter:datatype="link" oils_obj:required="true"/>
+			<field reporter:label="MARC Record Type" name="marc_record_type" reporter:datatype="text" oils_obj:required="true"/>
+			<field reporter:label="MARC Tag" name="tag" reporter:datatype="text" oils_obj:required="true"/>
+			<field reporter:label="MARC Subfield" name="code" reporter:datatype="text" oils_obj:required="true"/>
 			<field reporter:label="Description" name="description" reporter:datatype="text" oils_persist:i18n="true"/>
 			<field reporter:label="Repeatable?" name="repeatable" reporter:datatype="bool"/>
 			<field reporter:label="Mandatory?" name="mandatory" reporter:datatype="bool"/>
 			<field reporter:label="Hidden?" name="hidden" reporter:datatype="bool"/>
-			<field reporter:label="Owner" name="owner" reporter:datatype="link"/>
+			<field reporter:label="Owner" name="owner" reporter:datatype="org_unit"/>
         </fields>
 		<links>
 			<link field="marc_format" reltype="has_a" key="id" map="" class="cmrcfmt"/>

commit 2a8148341428dc0fc1c4947a148e5d996bc6db6b
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Fri May 13 11:51:42 2016 -0400

    webstaff: fix typo in field name
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 9ab8eef..adda25f 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -904,7 +904,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<field reporter:label="Name" name="name" reporter:datatype="text" oils_persist:i18n="true"/>
 			<field reporter:label="Description" name="description" reporter:datatype="text" oils_persist:i18n="true"/>
 			<field reporter:label="Fixed Field?" name="fixed_field" reporter:datatype="bool"/>
-			<field reporter:label="Repeatabl?" name="repeatable" reporter:datatype="bool"/>
+			<field reporter:label="Repeatable?" name="repeatable" reporter:datatype="bool"/>
 			<field reporter:label="Mandatory?" name="mandatory" reporter:datatype="bool"/>
 			<field reporter:label="Hidden?" name="hidden" reporter:datatype="bool"/>
 			<field reporter:label="Owner" name="owner" reporter:datatype="link"/>

commit bface48e0fff0512d46dee412bc46e8454521c5a
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed May 25 13:14:44 2016 -0400

    LP#1585369: Angular broke my copy editor!
    
    It seems Angular 1.5 is either less flexible or less forgiving in its
    ng-repeat implementation, and does not like an orderBy filter on object
    iterators.  Removing that, and the track by clause, allows the holdings
    editor to render properly.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
index 95e8e50..30ac2d3 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
@@ -65,7 +65,7 @@
         </div> <!-- row -->
         <eg-vol-edit
             focus-next="focusNextFirst"
-            ng-repeat="(lib,callnumbers) in data.tree | orderBy:lib track by lib"
+            ng-repeat="(lib,callnumbers) in data.tree"
             ng-init="ind = $index"
             record="{{record_id}}"
             only-vols="only_vols"
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 8c9ce68..0b109da 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -626,7 +626,7 @@ function(egCore , $q) {
                 '<div class="col-xs-1"><input ng-disabled="record == 0" class="form-control" type="number" min="{{orig_cn_count}}" ng-model="cn_count" ng-change="changeCNCount()"/></div>'+
                 '<div class="col-xs-10">'+
                     '<eg-vol-row only-vols="onlyVols" record="{{record}}"'+
-                        'ng-repeat="(cn,copies) in struct | orderBy:cn track by cn" '+
+                        'ng-repeat="(cn,copies) in struct" '+
                         'focus-next="focusNextFirst" copies="copies" allcopies="allcopies">'+
                     '</eg-vol-row>'+
                 '</div>'+

commit 28f7159c31279217d01258eb85d662fad2785680
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue May 17 22:09:34 2016 -0400

    webstaff: circ audio alerts
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
index 66d5335..7f953d6 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
@@ -384,6 +384,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         // Other events
         switch (evt[0].textcode) {
             case 'SUCCESS':
+                egCore.audio.play('success.renew');
                 return $q.when(final_resp);
 
             case 'COPY_IN_TRANSIT':
@@ -391,18 +392,21 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
             case 'PATRON_INACTIVE':
             case 'PATRON_ACCOUNT_EXPIRED':
             case 'CIRC_CLAIMS_RETURNED':
+                egCore.audio.play('warning.renew');
                 return service.exit_alert(
                     egCore.strings[evt[0].textcode],
                     {barcode : params.copy_barcode}
                 );
 
             case 'PERM_FAILURE':
+                egCore.audio.play('warning.renew.permission');
                 return service.exit_alert(
                     egCore.strings[evt[0].textcode],
                     {permission : evt[0].ilsperm}
                 );
 
             default:
+                egCore.audio.play('warning.renew.unknown');
                 return service.exit_alert(
                     egCore.strings.CHECKOUT_FAILED_GENERIC, {
                         barcode : params.copy_barcode,
@@ -431,6 +435,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
                 return $q.when(final_resp);
 
             case 'ITEM_NOT_CATALOGED':
+                egCore.audio.play('warning.checkout.no_cataloged');
                 return service.precat_dialog(params, options);
 
             case 'OPEN_CIRCULATION_EXISTS':
@@ -1240,12 +1245,14 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
                     case 4: /* MISSING */                                          
                     case 7: /* RESHELVING */ 
 
+                        egCore.audio.play('success.checkin');
+
                         // see if the copy location requires an alert
                         return service.handle_checkin_loc_alert(evt, params, options)
                         .then(function() {return final_resp});
 
                     case 8: /* ON HOLDS SHELF */
-
+                        egCore.audio.play('info.checkin.holds_shelf');
                         
                         if (hold) {
 
@@ -1262,6 +1269,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
                                 // normally, if the hold was on the shelf at a 
                                 // different location, it would be put into 
                                 // transit, resulting in a ROUTE_ITEM event.
+                                egCore.audio.play('warning.checkin.wrong_shelf');
                                 return $q.when(final_resp);
                             }
                         } else {
@@ -1272,14 +1280,17 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
                         }
 
                     case 11: /* CATALOGING */
+                        egCore.audio.play('info.checkin.cataloging');
                         evt[0].route_to = egCore.strings.ROUTE_TO_CATALOGING;
                         return $q.when(final_resp);
 
                     case 15: /* ON_RESERVATION_SHELF */
+                        egCore.audio.play('info.checkin.reservation');
                         // TODO: show booking reservation dialog
                         return $q.when(final_resp);
 
                     default:
+                        egCore.audio.play('error.checkin.unknown');
                         console.error('Unhandled checkin copy status: ' 
                             + copy.status().id() + ' : ' + copy.status().name());
                         return $q.when(final_resp);
@@ -1292,11 +1303,13 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
                 ).then(function() { return final_resp });
 
             case 'ASSET_COPY_NOT_FOUND':
+                egCore.audio.play('warning.checkin.not_found');
                 return egAlertDialog.open(
                     egCore.strings.UNCAT_ALERT_DIALOG, params)
                     .result.then(function() {return final_resp});
 
             case 'ITEM_NOT_CATALOGED':
+                egCore.audio.play('warning.checkin.not_cataloged');
                 evt[0].route_to = egCore.strings.ROUTE_TO_CATALOGING;
                 if (options.no_precat_alert) 
                     return $q.when(final_resp);
@@ -1305,6 +1318,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
                     .result.then(function() {return final_resp});
 
             default:
+                egCore.audio.play('error.checkin.unknown');
                 console.warn('unhandled checkin response : ' + evt[0].textcode);
                 return $q.when(final_resp);
         }
@@ -1373,6 +1387,10 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
                 print_context.patron = egCore.idl.toHash(data.patron);
             }
 
+            var sound = 'info.checkin.transit';
+            if (evt.payload.hold) sound += '.hold';
+            egCore.audio.play(sound);
+
             function print_transit() {
                 var template = data.transit ? 
                     (data.patron ? 'hold_transit_slip' : 'transit_slip') :
diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/holds.js b/Open-ILS/web/js/ui/default/staff/circ/services/holds.js
index 461f6ef..2b2ed77 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/services/holds.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/holds.js
@@ -89,6 +89,8 @@ function($uibModal , $q , egCore , egConfirmDialog , egAlertDialog) {
                                 $scope.args.note
                             ).then(function(resp) {
                                 if (evt = egCore.evt.parse(resp)) {
+                                    egCore.audio.play(
+                                        'warning.hold.cancel_failed');
                                     console.error('unable to cancel hold: ' 
                                         + evt.toString());
                                 }
@@ -137,6 +139,8 @@ function($uibModal , $q , egCore , egConfirmDialog , egAlertDialog) {
                                 egCore.auth.token(), hold_id
                             ).then(function(resp) {
                                 if (evt = egCore.evt.parse(resp)) {
+                                    egCore.audio.play(
+                                        'warning.hold.uncancel_failed');
                                     console.error('unable to uncancel hold: ' 
                                         + evt.toString());
                                 }
diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/transits.js b/Open-ILS/web/js/ui/default/staff/circ/services/transits.js
index 32781a1..ec2499e 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/services/transits.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/transits.js
@@ -45,6 +45,7 @@ function($uibModal , $q , egCore , egConfirmDialog , egAlertDialog) {
                                 egCore.auth.token(), { 'transitid' : transit.id() }
                             ).then(function(resp) {
                                 if (evt = egCore.evt.parse(resp)) {
+                                    egCore.audio.play('warning.transit.abort_failed');
                                     console.error('unable to abort transit: ' 
                                         + evt.toString());
                                 }

commit 676fdff4e78bcf33eb4f8c488d8995fcf9f75f65
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon May 16 23:16:34 2016 -0400

    webstaff: audio disable and testing options
    
    For workstation admin UI.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/staff/admin/workstation/t_splash.tt2 b/Open-ILS/src/templates/staff/admin/workstation/t_splash.tt2
index 8ceb795..360f6d7 100644
--- a/Open-ILS/src/templates/staff/admin/workstation/t_splash.tt2
+++ b/Open-ILS/src/templates/staff/admin/workstation/t_splash.tt2
@@ -92,6 +92,30 @@
   </div>
 
   <div class="row new-entry">
+    <div class="col-md-2">
+      <div class="checkbox">
+        <label>
+          <input type="checkbox"
+            ng-model="disable_sound" 
+              ng-change="apply_sound()">
+            [% l('Disable Sounds?') %]
+        </label>
+      </div>
+    </div>
+    <div class="col-md-4">
+      <span>Test: </span>
+      <button class="btn btn-success" ng-class="{disabled : disable_sound}" 
+        ng-click="test_audio('success')">[% l('Success') %]</button>
+      <button class="btn btn-info" ng-class="{disabled : disable_sound}" 
+        ng-click="test_audio('info')">[% l('Info') %]</button>
+      <button class="btn btn-warning" ng-class="{disabled : disable_sound}" 
+        ng-click="test_audio('warning')">[% l('Warning') %]</button>
+      <button class="btn btn-danger" ng-class="{disabled : disable_sound}" 
+        ng-click="test_audio('error')">[% l('Error') %]</button>
+    </div>
+  </div>
+
+  <div class="row new-entry">
     <div class="col-md-6">
       <span class="glyphicon glyphicon-print"></span>
       <a target="_self" href="./admin/workstation/print/config">
diff --git a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
index 4924f9d..d31c5fd 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
@@ -172,6 +172,23 @@ function($scope , $window , $location , egCore , egConfirmDialog) {
         egCore.hatch.setLocalItem(
             'eg.hatch.url', $scope.hatchURL);
     }
+
+    egCore.hatch.getItem('eg.audio.disable').then(function(val) {
+        $scope.disable_sound = val;
+    });
+
+    $scope.apply_sound = function() {
+        if ($scope.disable_sound) {
+            egCore.hatch.setItem('eg.audio.disable', true);
+        } else {
+            egCore.hatch.removeItem('eg.audio.disable');
+        }
+    }
+
+    $scope.test_audio = function(sound) {
+        egCore.audio.play(sound);
+    }
+
 }])
 
 .controller('PrintConfigCtrl',

commit d02c84fce8a3e99ce7be5357277b5958d8b04183
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun May 15 12:51:24 2016 -0400

    webstaff: egAudio HTML5 audio service
    
    egCore.audio.play('audio.event.dot.path');
    
    Service to look up audio URL's by key name.  Supports fall-thru behavior
    where 'foo.bar.baz' will fall-thru to 'foo.bar' and 'foo' depending on
    whether an audio file is avaialable.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/staff/base_js.tt2 b/Open-ILS/src/templates/staff/base_js.tt2
index 980ec5b..c1b721b 100644
--- a/Open-ILS/src/templates/staff/base_js.tt2
+++ b/Open-ILS/src/templates/staff/base_js.tt2
@@ -33,6 +33,7 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/startup.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/hatch.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/print.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/audio.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/coresvc.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/navbar.js"></script>
diff --git a/Open-ILS/web/audio/notifications/error.wav b/Open-ILS/web/audio/notifications/error.wav
new file mode 100644
index 0000000..41e23e0
Binary files /dev/null and b/Open-ILS/web/audio/notifications/error.wav differ
diff --git a/Open-ILS/web/audio/notifications/info.wav b/Open-ILS/web/audio/notifications/info.wav
new file mode 100644
index 0000000..6740d3f
Binary files /dev/null and b/Open-ILS/web/audio/notifications/info.wav differ
diff --git a/Open-ILS/web/audio/notifications/success.wav b/Open-ILS/web/audio/notifications/success.wav
new file mode 100644
index 0000000..eb83e16
Binary files /dev/null and b/Open-ILS/web/audio/notifications/success.wav differ
diff --git a/Open-ILS/web/audio/notifications/warning.wav b/Open-ILS/web/audio/notifications/warning.wav
new file mode 100644
index 0000000..76c4ecf
Binary files /dev/null and b/Open-ILS/web/audio/notifications/warning.wav differ
diff --git a/Open-ILS/web/js/ui/default/staff/Gruntfile.js b/Open-ILS/web/js/ui/default/staff/Gruntfile.js
index 6588ca8..910d7e1 100644
--- a/Open-ILS/web/js/ui/default/staff/Gruntfile.js
+++ b/Open-ILS/web/js/ui/default/staff/Gruntfile.js
@@ -141,6 +141,7 @@ module.exports = function(grunt) {
             'services/startup.js',
             'services/hatch.js',
             'services/print.js',
+            'services/audio.js',
             'services/coresvc.js',
             'services/navbar.js',
             'services/statusbar.js',
diff --git a/Open-ILS/web/js/ui/default/staff/services/audio.js b/Open-ILS/web/js/ui/default/staff/services/audio.js
new file mode 100644
index 0000000..1b88978
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/services/audio.js
@@ -0,0 +1,78 @@
+/**
+ * Core Service - egAudio
+ *
+ * Plays audio files by key name.  Each sound uses a dot-path to indicate 
+ * the sound.  
+ *
+ * For example:
+ * sound => 'warning.checkout.no_item'
+ * URLs are tested in the following order until a valid audio file is found
+ * or no other paths are left to check.
+ *
+ * /audio/notifications/warning/checkout/not_found.wav
+ * /audio/notifications/warning/checkout.wav
+ * /audio/notifications/warning.wav
+ *
+ * TODO: move audio file base path settings to the template 
+ * for configurability?
+ *
+ * Files are only played when sounds are configured to play via 
+ * workstation settings.
+ */
+
+angular.module('egCoreMod')
+
+.factory('egAudio', ['$q','egHatch', function($q, egHatch) {
+
+    var service = {
+        url_cache : {}, // map key names to audio file URLs
+        base_url : '/audio/notifications/'
+    };
+
+    /** 
+     * Play the sound found at the requested string path.  'path' is a 
+     * key name which maps to an audio file URL.
+     */
+    service.play = function(path) {
+        if (!path) return;
+        service.play_url(path, path);
+    }
+
+    service.play_url = function(path, orig_path) {
+
+        var url = service.url_cache[path] || 
+            service.base_url + path.replace(/\./g, '/') + '.wav';
+
+        var player = new Audio(url);
+
+        player.onloadeddata = function() {
+            console.debug('Playing audio URL: ' + url);
+            service.url_cache[orig_path] = url;
+            player.play();
+        };
+
+        if (service.url_cache[path]) {
+            // when serving from the cache, avoid secondary URL lookups.
+            return;
+        }
+
+        player.onerror = function() {
+            // Unable to play path at the requested URL.
+            
+            if (!path.match(/\./)) {
+                // all fall-through options have been exhausted.
+                // No path to play.
+                console.warn(
+                    "No suitable URL found for path '" + orig_path + "'");
+                return;
+            }
+
+            // Fall through to the next (more generic) option
+            path = path.replace(/\.[^\.]+$/, '');
+            service.play_url(path, orig_path);
+        }
+    }
+
+    return service;
+}]);
+
diff --git a/Open-ILS/web/js/ui/default/staff/services/coresvc.js b/Open-ILS/web/js/ui/default/staff/services/coresvc.js
index 57b8361..f754770 100644
--- a/Open-ILS/web/js/ui/default/staff/services/coresvc.js
+++ b/Open-ILS/web/js/ui/default/staff/services/coresvc.js
@@ -9,9 +9,11 @@ angular.module('egCoreMod')
 
 .factory('egCore', 
        ['egIDL','egNet','egEnv','egOrg','egPCRUD','egEvent','egAuth',
-        'egPerm','egHatch','egPrint','egStartup','egStrings','egDate',
+        'egPerm','egHatch','egPrint','egStartup','egStrings','egAudio',
+        'egDate',
 function(egIDL , egNet , egEnv , egOrg , egPCRUD , egEvent , egAuth ,
-         egPerm , egHatch , egPrint , egStartup , egStrings , egDate) {
+         egPerm , egHatch , egPrint , egStartup , egStrings , egAudio , 
+         egDate) {
 
     return {
         idl     : egIDL,
@@ -26,6 +28,7 @@ function(egIDL , egNet , egEnv , egOrg , egPCRUD , egEvent , egAuth ,
         print   : egPrint,
         startup : egStartup,
         strings : egStrings,
+        audio   : egAudio,
         date    : egDate
     };
 
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 06014df..760fc3b 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
@@ -34,14 +34,14 @@ module.exports = function(config){
       'services/org.js',
       'services/hatch.js',
       'services/print.js',
+      'services/audio.js',
       'services/coresvc.js',
       'services/user.js',
       'services/startup.js',
       'services/ui.js',
       'services/statusbar.js',
       'services/grid.js',
-      'services/navbar.js',
-      'services/date.js',
+      'services/navbar.js', 'services/date.js',
       // load app scripts
       'app.js',
       'circ/**/*.js',

commit eb53b50cf844ba7bfe3bf5f069cd6a9d80be868e
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu May 12 12:27:11 2016 -0400

    LP#1581126: webstaff: make egDateInput respect format.date OUS
    
    This patch makes the egDateInput directive fetch the
    date format from the format.date library setting. The
    directive also now accepts a dateFormat attribute for cases
    where there is a reason to override the library setting.
    
    If no format is set via library setting or in how the
    directive is invoked, the format defaults to "mediumDate",
    e.g., "May 2, 1999".
    
    To test:
    
    [1] Open the webstaff patron registration form. Verify that
        date widgets display the date in the format that
        corresponds to the value of the format.date library setting
        for the current work station, or (if the library setting
        is not set, "Month day, year".
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/share/t_datetime.tt2 b/Open-ILS/src/templates/staff/share/t_datetime.tt2
index 0e837f1..cc0afbf 100644
--- a/Open-ILS/src/templates/staff/share/t_datetime.tt2
+++ b/Open-ILS/src/templates/staff/share/t_datetime.tt2
@@ -5,7 +5,7 @@
       <input type="text"
         class="form-control"
         ng-show="!hideDatePicker"
-        uib-datepicker-popup="shortDate"
+        uib-datepicker-popup="{{date_format}}"
         is-open="datePickerIsOpen"
         ng-model="ngModel"
         ng-change="ngChange"
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 b9b3a8e..cbccf9a 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -459,8 +459,8 @@ function($window , egStrings) {
 * Handy wrapper directive for uib-datapicker-popup
 */
 .directive(
-    'egDateInput', ['egStrings',
-    function(egStrings) {
+    'egDateInput', ['egStrings', 'egCore',
+    function(egStrings, egCore) {
         return {
             scope : {
                 closeText : '@',
@@ -469,7 +469,8 @@ function($window , egStrings) {
                 ngBlur : '=',
                 ngDisabled : '=',
                 ngRequired : '=',
-                hideDatePicker : '='
+                hideDatePicker : '=',
+                dateFormat : '=?'
             },
             require: 'ngModel',
             templateUrl: './share/t_datetime',
@@ -480,6 +481,14 @@ function($window , egStrings) {
 
                 if ('showTimePicker' in attrs)
                     scope.showTimePicker = true;
+
+                var default_format = 'mediumDate';
+                egCore.org.settings(['format.date']).then(function(set) {
+                    default_format = set['format.date'];
+                    scope.date_format = (scope.dateFormat) ?
+                        scope.dateFormat :
+                        default_format;
+                });
             }
         };
     }

commit cea022c407e4193dcf3fed6b9ca81edfe04c3038
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Thu May 12 11:51:09 2016 -0400

    Add images for angular-tree-control to bower
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>

diff --git a/Open-ILS/web/js/ui/default/staff/Gruntfile.js b/Open-ILS/web/js/ui/default/staff/Gruntfile.js
index 0ba1afe..6588ca8 100644
--- a/Open-ILS/web/js/ui/default/staff/Gruntfile.js
+++ b/Open-ILS/web/js/ui/default/staff/Gruntfile.js
@@ -65,6 +65,27 @@ module.exports = function(grunt) {
             'bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff'
           ]
         }]
+      },
+
+      images : {
+        files : [{
+          dest : 'build/images/',
+          flatten : true,
+          filter : 'isFile',
+          expand : true,
+          src : [
+            'bower_components/angular-tree-control/images/sample.png',
+            'bower_components/angular-tree-control/images/node-opened-2.png',
+            'bower_components/angular-tree-control/images/folder.png',
+            'bower_components/angular-tree-control/images/node-closed.png',
+            'bower_components/angular-tree-control/images/node-closed-light.png',
+            'bower_components/angular-tree-control/images/node-opened.png',
+            'bower_components/angular-tree-control/images/node-opened-light.png',
+            'bower_components/angular-tree-control/images/folder-closed.png',
+            'bower_components/angular-tree-control/images/node-closed-2.png',
+            'bower_components/angular-tree-control/images/file.png'
+          ]
+        }]
       }
     },
 

commit f20039da9caa3e43bf626b65b667516e4383b0d5
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Wed May 11 16:31:33 2016 -0400

    webstaff: tweak legacy OU editor
    
    This adds a CSS hack so that the right-hand pane of
    the legacy OU editor is displayed when embedded in
    the web staff client.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/web/conify/global/actor/org_unit.html b/Open-ILS/web/conify/global/actor/org_unit.html
index 851a9ec..aca9335 100644
--- a/Open-ILS/web/conify/global/actor/org_unit.html
+++ b/Open-ILS/web/conify/global/actor/org_unit.html
@@ -44,6 +44,13 @@
 				padding-left: 20px;
 				padding-right: 5px;
 			}
+            /* this is a hack to get the right pane to display
+               in recent browsers when using this interface
+               embedded in the web staff client
+            */
+            .dijitLayoutContainer {
+                position: static;
+            }
 		</style>
 
 		<!-- The OpenSRF API writ JS -->

commit cce69273995f6f6668e5f5a0c52cafd4ed20f54d
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Wed May 11 16:30:34 2016 -0400

    webstaff: tweak permission group editor
    
    This adds a couple hackish formating tweaks so that
    the permission group editor works when embedded
    in the web staff client.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/web/conify/global/permission/grp_tree.html b/Open-ILS/web/conify/global/permission/grp_tree.html
index ac2026f..d6ebe49 100644
--- a/Open-ILS/web/conify/global/permission/grp_tree.html
+++ b/Open-ILS/web/conify/global/permission/grp_tree.html
@@ -57,6 +57,14 @@
                 height: 100%;
             }
 
+            /* this is a hack to get the right pane to display
+               in recent browsers when using this interface
+               embedded in the web staff client
+            */
+            .dijitLayoutContainer {
+                position: static;
+            }
+
 		</style>
 
 		<!-- The OpenSRF API writ JS -->
@@ -470,7 +478,7 @@
 							perm_map_model.refresh();
 							perm_grid.refresh();
 						</script>
-						<div dojoType="dijit.layout.LayoutContainer" orientation="horizontal" style="width:100%; height:100%;">
+						<div dojoType="dijit.layout.LayoutContainer" orientation="horizontal" style="width:100%; height:100%; min-height: 250px">
 							<div id="grid_container" dojoType="dijit.layout.ContentPane" sizeShare="1" layoutAlign="left">
 								<div dojoType="dojox.grid.data.DojoData" id="perm_map_model" jsId="perm_map_model" store="perm_map_store"></div>
 	

commit 7292174f182bba027362946c0a4a47d3ba2534da
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Wed May 11 13:17:55 2016 -0400

    webstaff: add server administration page
    
    This patch adds a landing page for Server Administration and
    code to embed all of the admin pages available under Serer
    Administration in the XUL staff client.
    
    TODO:
    
    * fix the permission groups editor so that it works
      when embedded in the web staff client
    * refactor admin/local/app.js and admin/server/app.js to
      unify some of the copy and pasting
    * replace the embedded legacy OU editor with a new
      angular one
    * fixes some CSS issues that make text hard to read in
      a few places
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/admin/server/index.tt2 b/Open-ILS/src/templates/staff/admin/server/index.tt2
new file mode 100644
index 0000000..362723a
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/server/index.tt2
@@ -0,0 +1,15 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Server Administration"); 
+  ctx.page_app = "egServerAdmin";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/eframe.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/admin/server/app.js"></script>
+<link rel="stylesheet" href="[% ctx.base_path %]/staff/css/admin.css" />
+[% END %]
+
+<div ng-view></div>
+
+[% END %]
diff --git a/Open-ILS/src/templates/staff/admin/server/t_splash.tt2 b/Open-ILS/src/templates/staff/admin/server/t_splash.tt2
new file mode 100644
index 0000000..1431942
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/server/t_splash.tt2
@@ -0,0 +1,78 @@
+
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    <span>[% l('Server Administration') %]</span>
+  </div>
+</div>
+
+<div class="container admin-splash-container">
+
+[%
+    interfaces = [
+     [ l('Actor Stat Cat Sip Fields'), "./admin/server/config/actor_sip_fields" ]
+    ,[ l('Age Hold Protect Rules'), "./admin/server/config/rule_age_hold_protect" ]
+    ,[ l('Asset Stat Cat Sip Fields'), "./admin/server/config/asset_sip_fields" ]
+    ,[ l('Authority Browse Axes'), "./admin/server/cat/authority/browse_axis" ]
+    ,[ l('Authority Control Sets'), "./admin/server/cat/authority/control_set" ]
+    ,[ l('Authority Thesauri'), "./admin/server/cat/authority/thesaurus" ]
+    ,[ l('Best-Hold Selection Sort Order'), "./admin/server/config/best_hold_order" ]
+    ,[ l('Billing Types'), "./admin/server/config/billing_type" ]
+    ,[ l('Booking Resource Attribute Maps'), "./admin/server/booking/resource_attr_map" ]
+    ,[ l('Booking Resource Attribute Values'), "./admin/server/booking/resource_attr_value" ]
+    ,[ l('Booking Resource Attributes'), "./admin/server/booking/resource_attr" ]
+    ,[ l('Booking Resource Types'), "./admin/server/booking/resource_type" ]
+    ,[ l('Booking Resources'), "./admin/server/booking/resource" ]
+    ,[ l('Call Number Prefixes'), "./admin/server/config/acn_prefix" ]
+    ,[ l('Call Number Suffixes'), "./admin/server/config/acn_suffix" ]
+    ,[ l('Circulation Duration Rules'), "./admin/server/config/rule_circ_duration" ]
+    ,[ l('Circulation Limit Groups'), "./admin/server/config/circ_limit_group" ]
+    ,[ l('Circulation Matchpoint Weights'), "./admin/server/config/circ_matrix_weights" ]
+    ,[ l('Circulation Max Fine Rules'), "./admin/server/config/rule_max_fine" ]
+    ,[ l('Circulation Modifiers'), "./admin/server/config/circ_modifier" ]
+    ,[ l('Circulation Recurring Fine Rules'), "./admin/server/config/rule_recurring_fine" ]
+    ,[ l('Copy Statuses'), "./admin/server/legacy/config/copy_status" ]
+    ,[ l('Custom Org Unit Trees'), "./admin/server/actor/org_unit_custom_tree" ]
+    ,[ l('Floating Groups'), "./admin/server/config/floating_groups" ]
+    ,[ l('Global Flags'), "./admin/server/config/global_flag" ]
+    ,[ l('Hard Due Date Changes'), "./admin/server/config/hard_due_date" ]
+    ,[ l('Hold Matchpoint Weights'), "./admin/server/config/hold_matrix_weights" ]
+    ,[ l('Import Match Sets'), "./admin/server/vandelay/match_set" ]
+    ,[ l('MARC Coded Value Maps'), "./admin/server/config/coded_value_map" ]
+    ,[ l('MARC Import Remove Fields'), "./admin/server/vandelay/import_bib_trash_group" ]
+    ,[ l('MARC Record Attributes'), "./admin/server/config/record_attr_definition" ]
+    ,[ l('MARC Search/Facet Class FTS Maps'), "./admin/server/config/metabib_class_ts_map" ]
+    ,[ l('MARC Search/Facet Classes'), "./admin/server/config/metabib_class" ]
+    ,[ l('MARC Search/Facet Field FTS Maps'), "./admin/server/config/metabib_field_ts_map" ]
+    ,[ l('MARC Search/Facet Fields'), "./admin/server/config/metabib_field" ]
+    ,[ l('Org Unit Proximity Adjustments'), "./admin/server/config/org_unit_proximity_adjustment" ]
+    ,[ l('Organization Types'), "./admin/server/legacy/actor/org_unit_type" ]
+    ,[ l('Org Unit Setting Types'), "./admin/server/config/org_unit_setting_type" ]
+    ,[ l('Organizational Units'), "./admin/server/legacy/actor/org_unit" ]
+    ,[ l('Permission Groups'), "./admin/server/legacy/permission/grp_tree" ]
+    ,[ l('Permissions'), "./admin/server/legacy/permission/perm_list" ]
+    ,[ l('Remote Accounts'), "./admin/server/config/remote_account" ]
+    ,[ l('SMS Carriers'), "./admin/server/config/sms_carrier" ]
+    ,[ l('User Activity Types'), "./admin/server/config/usr_activity_type" ]
+    ,[ l('User Setting Types'), "./admin/server/config/usr_setting_type" ]
+    ,[ l('Weights Association'), "./admin/server/config/weight_assoc" ]
+    ,[ l('Z39.50 Index Field Maps'), "./admin/server/config/z3950_index_field_map" ]
+    ,[ l('Z39.50 Servers'), "./admin/server/config/z3950_source" ]
+   ];
+
+   USE table(interfaces, rows=16);
+%]
+
+[% FOREACH row = table.rows %]
+  <div class="row new-entry">
+    [% FOREACH item = row %][% IF item.1 %]
+    <div class="col-md-4">
+      <span class="glyphicon glyphicon-pencil"></span>
+      <a target="_self" href="[% item.1 %]">
+        [% item.0 %]
+      </a>
+    </div>
+    [% END %][% END %]
+  </div>
+[% END %]
+
+</div>
diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2
index c4e765b..cce234b 100644
--- a/Open-ILS/src/templates/staff/navbar.tt2
+++ b/Open-ILS/src/templates/staff/navbar.tt2
@@ -274,6 +274,12 @@
             </a>
           </li>
           <li>
+            <a href="./admin/server/index" target="_self">
+              <span class="glyphicon glyphicon-briefcase"></span>
+              [% l('Server Administration') %]
+            </a>
+          </li>
+          <li>
             <a href="./admin/local/index" target="_self">
               <span class="glyphicon glyphicon-picture"></span>
               [% l('Local Administration') %]
diff --git a/Open-ILS/web/js/ui/default/staff/admin/server/app.js b/Open-ILS/web/js/ui/default/staff/admin/server/app.js
new file mode 100644
index 0000000..a28987a
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/admin/server/app.js
@@ -0,0 +1,80 @@
+angular.module('egServerAdmin',
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod','egUiMod'])
+
+.config(['$routeProvider','$locationProvider','$compileProvider', 
+ function($routeProvider , $locationProvider , $compileProvider) {
+
+    $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); 
+    var resolver = {delay : function(egStartup) {return egStartup.go()}};
+
+    var eframe_template = 
+        '<eg-embed-frame url="server_admin_url" handlers="funcs"></eg-embed-frame>';
+
+    // old-style Confiy
+    $routeProvider.when('/admin/server/legacy/:schema/:page', {
+        template: eframe_template,
+        controller: 'EmbedOldConifyCtl',
+        resolve : resolver
+    });
+   
+    // Conify page handler (some authority admin interfaces live
+    // under global/cat/authority/)
+    $routeProvider.when('/admin/server/:module/:schema/:page', {
+        template: eframe_template,
+        controller: 'EmbedConifyCtl',
+        resolve : resolver
+    });
+
+    // Conify page handler
+    $routeProvider.when('/admin/server/:schema/:page', {
+        template: eframe_template,
+        controller: 'EmbedConifyCtl',
+        resolve : resolver
+    });
+
+    // default page 
+    $routeProvider.otherwise({
+        templateUrl : './admin/server/t_splash',
+        resolve : resolver
+    });
+}])
+
+.controller('EmbedConifyCtl', 
+       ['$scope','$routeParams','$location','egCore',
+function($scope , $routeParams , $location , egCore) {
+
+    $scope.funcs = {
+        ses : egCore.auth.token(),
+    }
+    var conify_path = '/eg/conify/global/' +
+        (angular.isDefined($routeParams.module) ? ($routeParams.module + '/') : '') +
+        $routeParams.schema + '/' + $routeParams.page;
+
+    // embed URL must include protocol/domain or it will be loaded via
+    // push-state, resulting in an infinitely nested pages.
+    $scope.server_admin_url = 
+        $location.absUrl().replace(/\/eg\/staff.*/, conify_path);
+
+    console.log('Loading server admin URL: ' + $scope.server_admin_url);
+
+}])
+
+.controller('EmbedOldConifyCtl', 
+       ['$scope','$routeParams','$location','egCore',
+function($scope , $routeParams , $location , egCore) {
+
+    $scope.funcs = {
+        ses : egCore.auth.token(),
+    }
+    var conify_path = '/conify/global/' +
+        $routeParams.schema + '/' + $routeParams.page + '.html';
+
+    // embed URL must include protocol/domain or it will be loaded via
+    // push-state, resulting in an infinitely nested pages.
+    $scope.server_admin_url = 
+        $location.absUrl().replace(/\/eg\/staff.*/, conify_path);
+
+    console.log('Loading server admin URL: ' + $scope.server_admin_url);
+
+}])

commit 84df55949e502b16c724fdfdcc8dcafa59dedd28
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Wed May 4 15:04:23 2016 -0400

    webstaff: work log: various improvements
    
    * Don't use $location.path() to generate a URL for when opening
      a new window, as that can cause the parent window to reset
      itself.
    * Fix the refresh button
    * Add support for paging through worklog entries
    * Don't prematurely resolve the promise that is
      feeding rows to the grid
    * Ensure that entries are displayed in timestamp order
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/web/js/ui/default/staff/admin/workstation/log.js b/Open-ILS/web/js/ui/default/staff/admin/workstation/log.js
index b9fe1db..b2411e2 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/workstation/log.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/workstation/log.js
@@ -18,8 +18,8 @@ angular.module('egWorkLogApp',
 })
 
 .controller('WorkLogCtrl',
-       ['$scope','$q','$routeParams','$window','$location','$timeout','egCore','egGridDataProvider','egWorkLog',
-function($scope , $q , $routeParams , $window , $location , $timeout , egCore , egGridDataProvider , egWorkLog ) {
+       ['$scope','$q','$routeParams','$window','$timeout','egCore','egGridDataProvider','egWorkLog',
+function($scope , $q , $routeParams , $window , $timeout , egCore , egGridDataProvider , egWorkLog ) {
 
     var work_log_entries = [];
     var patron_log_entries = [];
@@ -34,9 +34,7 @@ function($scope , $q , $routeParams , $window , $location , $timeout , egCore ,
         if (!angular.isArray(log_entries)) log_entries = [log_entries];
         angular.forEach(log_entries, function(log_entry) {
             $window.open(
-                $location.path(
-                    '/cat/item/' + log_entry.item_id
-                ).absUrl(),
+                egCore.env.basePath + '/cat/item/' + log_entry.item_id,
                 '_blank'
             ).focus();
         });
@@ -51,9 +49,8 @@ function($scope , $q , $routeParams , $window , $location , $timeout , egCore ,
         if (!angular.isArray(log_entries)) log_entries = [log_entries];
         angular.forEach(log_entries, function(log_entry) {
             $window.open(
-                $location.path(
-                    '/circ/patron/' + log_entry.patron_id + '/checkout'
-                ).absUrl(),
+                egCore.env.basePath +
+                '/circ/patron/' + log_entry.patron_id + '/checkout',
                 '_blank'
             ).focus();
         });
@@ -67,14 +64,15 @@ function($scope , $q , $routeParams , $window , $location , $timeout , egCore ,
         activateItem : load_patron
     }
 
-    function refresh_page() {
+    $scope.refresh_ui = function() {
         work_log_entries = [];
         patron_log_entries = [];
-        provider.refresh();
+        work_log_provider.refresh();
+        patron_log_provider.refresh();
     }
 
     function fetch_hold(deferred,entry) {
-        egCore.pcrud.search('ahr',
+        return egCore.pcrud.search('ahr',
             { 'id' : entry.data.hold_id }, {
                 'flesh' : 2,
                 'flesh_fields' : {
@@ -82,26 +80,22 @@ function($scope , $q , $routeParams , $window , $location , $timeout , egCore ,
                 },
             }
         ).then(
-            deferred.resolve, null, 
             function(hold) {
                 entry.patron_id = hold.usr().id();
                 entry.user = hold.usr().family_name();
                 if (hold.current_copy()) {
                     entry.item = hold.current_copy().barcode();
                 }
-                deferred.notify(entry);
             }
         );
     }
 
     function fetch_patron(deferred,entry) {
-        egCore.pcrud.search('au',
+        return egCore.pcrud.search('au',
             { 'id' : entry.data.patron_id }, {}
         ).then(
-            deferred.resolve, null,
             function(usr) {
                 entry.user = usr.family_name();
-                deferred.notify(entry);
             }
         );
     }
@@ -111,24 +105,29 @@ function($scope , $q , $routeParams , $window , $location , $timeout , egCore ,
         console.log(log_entries);
         var deferred = $q.defer();
 
-        $timeout( function() {
-            log_entries.work_log.forEach(
-                function(el,idx) {
-                    el.id = idx;
-                    if (el.action == 'requested_hold') {
-                        fetch_hold(deferred,el);
-                    } else if (el.action == 'registered_patron') {
-                        fetch_patron(deferred,el);
-                    } else if (el.action == 'edited_patron') {
-                        fetch_patron(deferred,el);
-                    } else if (el.action == 'paid_bill') {
-                        fetch_patron(deferred,el);
-                    } else {
-                        deferred.notify(el);
-                    }
+        var promises = [];
+        var entries = count ?
+                      log_entries.work_log.slice(offset, offset + count) :
+                      log_entries.work_log;
+        entries.forEach(
+            function(el,idx) {
+                el.id = idx;
+                // notify right away and in order; fetch_* will
+                // fill in entry later if necessary
+                promises.push($timeout(function() { deferred.notify(el) }));
+                if (el.action == 'requested_hold') {
+                    promises.push(fetch_hold(deferred,el));
+                } else if (el.action == 'registered_patron') {
+                    promises.push(fetch_patron(deferred,el));
+                } else if (el.action == 'edited_patron') {
+                    promises.push(fetch_patron(deferred,el));
+                } else if (el.action == 'paid_bill') {
+                    promises.push(fetch_patron(deferred,el));
                 }
-            );
-        });
+            }
+        );
+        $q.all(promises).then(deferred.resolve);
+
         return deferred.promise;
     }
 
@@ -137,24 +136,29 @@ function($scope , $q , $routeParams , $window , $location , $timeout , egCore ,
         console.log(log_entries);
         var deferred = $q.defer();
 
-        $timeout( function() {
-            log_entries.patron_log.forEach(
-                function(el,idx) {
-                    el.id = idx;
-                    if (el.action == 'requested_hold') {
-                        fetch_hold(deferred,el);
-                    } else if (el.action == 'registered_patron') {
-                        fetch_patron(deferred,el);
-                    } else if (el.action == 'edited_patron') {
-                        fetch_patron(deferred,el);
-                    } else if (el.action == 'paid_bill') {
-                        fetch_patron(deferred,el);
-                    } else {
-                        deferred.notify(el);
-                    }
+        var promises = [];
+        var entries = count ?
+                      log_entries.patron_log.slice(offset, offset + count) :
+                      log_entries.patron_log;
+        log_entries.patron_log.forEach(
+            function(el,idx) {
+                el.id = idx;
+                // notify right away and in order; fetch_* will
+                // fill in entry later if necessary
+                promises.push($timeout(function() { deferred.notify(el) }));
+                if (el.action == 'requested_hold') {
+                    promises.push(fetch_hold(deferred,el));
+                } else if (el.action == 'registered_patron') {
+                    promises.push(fetch_patron(deferred,el));
+                } else if (el.action == 'edited_patron') {
+                    promises.push(fetch_patron(deferred,el));
+                } else if (el.action == 'paid_bill') {
+                    promises.push(fetch_patron(deferred,el));
                 }
-            );
-        });
+            }
+        );
+        $q.all(promises).then(deferred.resolve);
+
         return deferred.promise;
     }
 

commit 51fe14771d2f117f0f980047f53a64772f2c2ba1
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Fri Dec 4 11:06:56 2015 -0500

    webstaff: egWorkLog service and Work Log UI
    
    under Administration -> Local Administration
    
    The original XUL feature starts here: 29d1b357eef061bb3698e4ce0506eb93b63421be
    
    Make sure egCore from the calling interface is pulling in these org unit
    settings:
    
    ui.admin.work_log.max_entries
    ui.admin.patron_log.max_entries
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/opac/parts/place_hold_result.tt2 b/Open-ILS/src/templates/opac/parts/place_hold_result.tt2
index 87fca56..889f411 100644
--- a/Open-ILS/src/templates/opac/parts/place_hold_result.tt2
+++ b/Open-ILS/src/templates/opac/parts/place_hold_result.tt2
@@ -59,13 +59,17 @@
                                 window.addEventListener(
                                     'load',
                                     function() {
-                                        try {
-                                            if (typeof xulG != 'undefined' && xulG.opac_hold_placed) {
-                                                xulG.opac_hold_placed([% hdata.hold_success %]);
-                                            }
-                                        } catch(E) {
-                                            alert('Error updating Work Log with hold placement: ' + E);
-                                        }
+                                        setTimeout( // we want this to run _after_ other onload handlers (such as from eframe.js)
+                                            function() {
+                                                try {
+                                                    if (typeof xulG != 'undefined' && xulG.opac_hold_placed) {
+                                                        xulG.opac_hold_placed([% hdata.hold_success %]);
+                                                    }
+                                                } catch(E) {
+                                                    alert('Error updating Work Log with hold placement: ' + E);
+                                                }
+                                            }, 0
+                                        );
                                     },
                                     false
                                 );
diff --git a/Open-ILS/src/templates/staff/admin/workstation/log.tt2 b/Open-ILS/src/templates/staff/admin/workstation/log.tt2
new file mode 100644
index 0000000..3e5cf0a
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/workstation/log.tt2
@@ -0,0 +1,17 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Work Log"); 
+  ctx.page_app = "egWorkLogApp";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/admin/workstation/log.js"></script>
+<link rel="stylesheet" href="[% ctx.base_path %]/staff/css/circ.css" />
+[% END %]
+
+<div ng-view></div>
+
+[% END %]
diff --git a/Open-ILS/src/templates/staff/admin/workstation/t_log.tt2 b/Open-ILS/src/templates/staff/admin/workstation/t_log.tt2
new file mode 100644
index 0000000..014d74d
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/workstation/t_log.tt2
@@ -0,0 +1,59 @@
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    <span>[% l('Work Log') %]</span>
+  </div>
+</div>
+
+<eg-grid
+  main-label="[% l('Most Recently Logged Staff Actions') %]"
+  id-field="id"
+  features="-sort,-multisort"
+  items-provider="grid_work_log_provider"
+  grid-controls="grid_controls"
+  persist-key="admin.workstation.work_log"
+>
+
+  <eg-grid-menu-item handler="refresh_ui" 
+    label="[% l('Refresh') %]"></eg-grid-menu-item>
+
+  <eg-grid-menu-item handler="load_item" 
+    label="[% l('Retrieve Item') %]"></eg-grid-menu-item>
+
+  <eg-grid-menu-item handler="load_patron" 
+    label="[% l('Retrieve Patron') %]"></eg-grid-menu-item>
+
+  <eg-grid-field path='action' label="[% l('Code') %]" hidden></eg-grid-field>
+  <eg-grid-field path='msg' label="[% l('Message') %]"></eg-grid-field>
+  <eg-grid-field path='amount' label="[% l('Amount') %]" hidden></eg-grid-field>
+  <eg-grid-field path='user' label="[% l('Patron') %]"></eg-grid-field>
+  <eg-grid-field path='item' label="[% l('Item') %]"></eg-grid-field>
+  <eg-grid-field path='when' label="[% l('When') %]"></eg-grid-field>
+  <eg-grid-field path='actor' label="[% l('Staff') %]" hidden></eg-grid-field>
+</eg-grid>
+
+<hr/>
+
+<eg-grid
+  main-label="[% l('Most Recently Affected Patrons') %]"
+  id-field="id"
+  features="-sort,-multisort"
+  items-provider="grid_patron_log_provider"
+  grid-controls="grid_controls"
+  persist-key="admin.workstation.patron_log"
+>
+
+  <eg-grid-menu-item handler="load_item" 
+    label="[% l('Retrieve Item') %]"></eg-grid-menu-item>
+
+  <eg-grid-menu-item handler="load_patron" 
+    label="[% l('Retrieve Patron') %]"></eg-grid-menu-item>
+
+  <eg-grid-field path='action' label="[% l('Code') %]" hidden></eg-grid-field>
+  <eg-grid-field path='msg' label="[% l('Message') %]"></eg-grid-field>
+  <eg-grid-field path='amount' label="[% l('Amount') %]" hidden></eg-grid-field>
+  <eg-grid-field path='user' label="[% l('Patron') %]"></eg-grid-field>
+  <eg-grid-field path='item' label="[% l('Item') %]"></eg-grid-field>
+  <eg-grid-field path='when' label="[% l('When') %]"></eg-grid-field>
+  <eg-grid-field path='actor' label="[% l('Staff') %]" hidden></eg-grid-field>
+</eg-grid>
+
diff --git a/Open-ILS/src/templates/staff/base_js.tt2 b/Open-ILS/src/templates/staff/base_js.tt2
index 54e6e7f..980ec5b 100644
--- a/Open-ILS/src/templates/staff/base_js.tt2
+++ b/Open-ILS/src/templates/staff/base_js.tt2
@@ -60,6 +60,19 @@
     s.EG_UNLOAD_PAGE_PROMPT_MSG = 
       '[% l('This page may have unsaved data.') %]';
     s.EG_DATE_INPUT_CLOSE_TEXT = '[% l('Close') %]';
+    s.EG_WORK_LOG_CHECKOUT = '[% l('Check Out') %]';
+    s.EG_WORK_LOG_RENEW = '[% l('Renew') %]';
+    s.EG_WORK_LOG_CHECKIN = '[% l('Check In') %]';
+    s.EG_WORK_LOG_EDITED_PATRON = '[% l('Edited Patron') %]';
+    s.EG_WORK_LOG_REGISTERED_PATRON = '[% l('Registered Patron') %]';
+    s.EG_WORK_LOG_CASH_PAYMENT = '[% l('Cash Payment') %]';
+    s.EG_WORK_LOG_CHECK_PAYMENT = '[% l('Check Payment') %]';
+    s.EG_WORK_LOG_CREDIT_CARD_PAYMENT = '[% l('Credit Card Payment') %]';
+    s.EG_WORK_LOG_CREDIT_PAYMENT = '[% l('Credit Payment') %]';
+    s.EG_WORK_LOG_WORK_PAYMENT = '[% l('Work Payment') %]';
+    s.EG_WORK_LOG_FORGIVE_PAYMENT = '[% l('Forgive Payment') %]';
+    s.EG_WORK_LOG_GOODS_PAYMENT = '[% l('Goods Payment') %]';
+    s.EG_WORK_LOG_REQUESTED_HOLD = '[% l('Hold Request') %]';
   }]);
 </script>
 
diff --git a/Open-ILS/src/templates/staff/css/style.css.tt2 b/Open-ILS/src/templates/staff/css/style.css.tt2
index d4e4ae5..fb06c0e 100644
--- a/Open-ILS/src/templates/staff/css/style.css.tt2
+++ b/Open-ILS/src/templates/staff/css/style.css.tt2
@@ -220,6 +220,7 @@ table.list tr.selected td { /* deprecated? */
 .eg-grid-primary-label {
   font-weight: bold;
   font-size: 120%;
+  margin-right: 2em;
 }
 
 /* odd/even row styling */
diff --git a/Open-ILS/web/js/ui/default/staff/admin/workstation/log.js b/Open-ILS/web/js/ui/default/staff/admin/workstation/log.js
new file mode 100644
index 0000000..b9fe1db
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/admin/workstation/log.js
@@ -0,0 +1,162 @@
+angular.module('egWorkLogApp', 
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+    $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
+
+    var resolver = {delay : 
+        ['egStartup', function(egStartup) {return egStartup.go()}]}
+
+    $routeProvider.when('/admin/workstation/log', {
+        templateUrl: './admin/workstation/t_log',
+        controller: 'WorkLogCtrl',
+        resolve : resolver
+    });
+
+    $routeProvider.otherwise({redirectTo : '/admin/workstation/log'});
+})
+
+.controller('WorkLogCtrl',
+       ['$scope','$q','$routeParams','$window','$location','$timeout','egCore','egGridDataProvider','egWorkLog',
+function($scope , $q , $routeParams , $window , $location , $timeout , egCore , egGridDataProvider , egWorkLog ) {
+
+    var work_log_entries = [];
+    var patron_log_entries = [];
+
+    var work_log_provider = egGridDataProvider.instance({});
+    var patron_log_provider = egGridDataProvider.instance({});
+    $scope.grid_work_log_provider = work_log_provider;
+    $scope.grid_patron_log_provider = patron_log_provider;
+
+    function load_item(log_entries) {
+        if (!log_entries) return;
+        if (!angular.isArray(log_entries)) log_entries = [log_entries];
+        angular.forEach(log_entries, function(log_entry) {
+            $window.open(
+                $location.path(
+                    '/cat/item/' + log_entry.item_id
+                ).absUrl(),
+                '_blank'
+            ).focus();
+        });
+    }
+
+    $scope.load_item = function(action, data, entries) {
+        load_item(entries);
+    }
+
+    function load_patron(log_entries) {
+        if (!log_entries) return;
+        if (!angular.isArray(log_entries)) log_entries = [log_entries];
+        angular.forEach(log_entries, function(log_entry) {
+            $window.open(
+                $location.path(
+                    '/circ/patron/' + log_entry.patron_id + '/checkout'
+                ).absUrl(),
+                '_blank'
+            ).focus();
+        });
+    }
+
+    $scope.load_patron = function(action, data, entries) {
+        load_patron(entries);
+    }
+
+    $scope.grid_controls = {
+        activateItem : load_patron
+    }
+
+    function refresh_page() {
+        work_log_entries = [];
+        patron_log_entries = [];
+        provider.refresh();
+    }
+
+    function fetch_hold(deferred,entry) {
+        egCore.pcrud.search('ahr',
+            { 'id' : entry.data.hold_id }, {
+                'flesh' : 2,
+                'flesh_fields' : {
+                    'ahr' : ['usr','current_copy'],
+                },
+            }
+        ).then(
+            deferred.resolve, null, 
+            function(hold) {
+                entry.patron_id = hold.usr().id();
+                entry.user = hold.usr().family_name();
+                if (hold.current_copy()) {
+                    entry.item = hold.current_copy().barcode();
+                }
+                deferred.notify(entry);
+            }
+        );
+    }
+
+    function fetch_patron(deferred,entry) {
+        egCore.pcrud.search('au',
+            { 'id' : entry.data.patron_id }, {}
+        ).then(
+            deferred.resolve, null,
+            function(usr) {
+                entry.user = usr.family_name();
+                deferred.notify(entry);
+            }
+        );
+    }
+
+    work_log_provider.get = function(offset, count) {
+        var log_entries = egWorkLog.retrieve_all();
+        console.log(log_entries);
+        var deferred = $q.defer();
+
+        $timeout( function() {
+            log_entries.work_log.forEach(
+                function(el,idx) {
+                    el.id = idx;
+                    if (el.action == 'requested_hold') {
+                        fetch_hold(deferred,el);
+                    } else if (el.action == 'registered_patron') {
+                        fetch_patron(deferred,el);
+                    } else if (el.action == 'edited_patron') {
+                        fetch_patron(deferred,el);
+                    } else if (el.action == 'paid_bill') {
+                        fetch_patron(deferred,el);
+                    } else {
+                        deferred.notify(el);
+                    }
+                }
+            );
+        });
+        return deferred.promise;
+    }
+
+    patron_log_provider.get = function(offset, count) {
+        var log_entries = egWorkLog.retrieve_all();
+        console.log(log_entries);
+        var deferred = $q.defer();
+
+        $timeout( function() {
+            log_entries.patron_log.forEach(
+                function(el,idx) {
+                    el.id = idx;
+                    if (el.action == 'requested_hold') {
+                        fetch_hold(deferred,el);
+                    } else if (el.action == 'registered_patron') {
+                        fetch_patron(deferred,el);
+                    } else if (el.action == 'edited_patron') {
+                        fetch_patron(deferred,el);
+                    } else if (el.action == 'paid_bill') {
+                        fetch_patron(deferred,el);
+                    } else {
+                        deferred.notify(el);
+                    }
+                }
+            );
+        });
+        return deferred.promise;
+    }
+
+}])
+
diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/bills.js b/Open-ILS/web/js/ui/default/staff/circ/patron/bills.js
index c9b6c44..1245a2b 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/patron/bills.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/patron/bills.js
@@ -4,8 +4,8 @@
 angular.module('egPatronApp')
 
 .factory('billSvc', 
-       ['$q','egCore','patronSvc',
-function($q , egCore , patronSvc) {
+       ['$q','egCore','egWorkLog','patronSvc',
+function($q , egCore , egWorkLog , patronSvc) {
 
     var service = {};
 
@@ -39,6 +39,24 @@ function($q , egCore , patronSvc) {
             patronSvc.current.last_xact_id()
         ).then(function(resp) {
             console.debug('payments: ' + js2JSON(resp));
+            var total = 0; angular.forEach(payments,function(p) { total += p[1]; });
+            var msg;
+            switch(type) {
+                case 'cash_payment' : msg = egCore.strings.EG_WORK_LOG_CASH_PAYMENT; break;
+                case 'check_payment' : msg = egCore.strings.EG_WORK_LOG_CHECK_PAYMENT; break;
+                case 'credit_card_payment' : msg = egCore.strings.EG_WORK_LOG_CREDIT_CARD_PAYMENT; break;
+                case 'credit_payment' : msg = egCore.strings.EG_WORK_LOG_CREDIT_PAYMENT; break;
+                case 'work_payment' : msg = egCore.strings.EG_WORK_LOG_WORK_PAYMENT; break;
+                case 'forgive_payment' : msg = egCore.strings.EG_WORK_LOG_FORGIVE_PAYMENT; break;
+                case 'goods_payment' : msg = egCore.strings.EG_WORK_LOG_GOODS_PAYMENT; break;
+            }
+            egWorkLog.record(
+                msg,{
+                    'action' : 'paid_bill',
+                    'patron_id' : service.userId,
+                    'total_amount' : total
+                }
+            );
             if (evt = egCore.evt.parse(resp)) 
                 return alert(evt);
 
diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/holds.js b/Open-ILS/web/js/ui/default/staff/circ/patron/holds.js
index 9c2101f..ddbf1d8 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/patron/holds.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/patron/holds.js
@@ -137,13 +137,19 @@ function($scope,  $q,  $routeParams,  egCore,  egUser,  patronSvc,
 
 
 .controller('PatronHoldsCreateCtrl',
-       ['$scope','$routeParams','$location','egCore','patronSvc',
-function($scope , $routeParams , $location , egCore , patronSvc) {
+       ['$scope','$routeParams','$location','egCore','egWorkLog','patronSvc',
+function($scope , $routeParams , $location , egCore , egWorkLog , patronSvc) {
 
     $scope.handlers = {
-        opac_hold_placed : function() {
-            // FIXME: this isn't getting called.. not sure why
+        opac_hold_placed : function(hold) {
             patronSvc.fetchUserStats(); // update hold counts
+            egWorkLog.record(
+                egCore.strings.EG_WORK_LOG_REQUESTED_HOLD,{
+                    'action' : 'requested_hold',
+                    'patron_id' : patronSvc.current.id(),
+                    'hold_id' : hold
+                }
+            );
         }
     }
 
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 0e6629e..d08d699 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
@@ -348,9 +348,15 @@ angular.module('egCoreMod')
             'sms.enable',
             'ui.patron.edit.aua.state.require',
             'ui.patron.edit.aua.state.suggest',
-            'ui.patron.edit.aua.state.show'
+            '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 service.process_org_settings(settings);
         });
     };
@@ -1078,11 +1084,13 @@ angular.module('egCoreMod')
     return service;
 }])
 
-.controller('PatronRegCtrl', 
+.controller('PatronRegCtrl',
        ['$scope','$routeParams','$q','$uibModal','$window','egCore',
         'patronSvc','patronRegSvc','egUnloadPrompt','egAlertDialog',
-function($scope , $routeParams , $q , $uibModal , $window , egCore , 
-         patronSvc , patronRegSvc , egUnloadPrompt, egAlertDialog) {
+        'egWorkLog',
+function($scope , $routeParams , $q , $uibModal , $window , egCore ,
+         patronSvc , patronRegSvc , egUnloadPrompt, egAlertDialog ,
+         egWorkLog) {
 
     $scope.page_data_loaded = false;
     $scope.clone_id = patronRegSvc.clone_id = $routeParams.clone_id;
@@ -1801,6 +1809,17 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
 
         }).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.
@@ -1821,4 +1840,3 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         });
     }
 }])
-
diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
index e74cc78..66d5335 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
@@ -5,9 +5,10 @@
 angular.module('egCoreMod')
 
 .factory('egCirc',
-
        ['$uibModal','$q','egCore','egAlertDialog','egConfirmDialog',
-function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog) {
+        'egWorkLog',
+function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
+         egWorkLog) {
 
     var service = {
         // auto-override these events after the first override
@@ -17,7 +18,9 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog) {
 
     egCore.startup.go().finally(function() {
         egCore.org.settings([
-            'ui.staff.require_initials.patron_standing_penalty'
+            'ui.staff.require_initials.patron_standing_penalty',
+            'ui.admin.work_log.max_entries',
+            'ui.admin.patron_log.max_entries'
         ]).then(function(set) {
             service.require_initials = Boolean(set['ui.staff.require_initials.patron_standing_penalty']);
         });
@@ -131,7 +134,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog) {
                     return service.handle_checkout_resp(evt, params, options);
                 })
                 .then(function(final_resp) {
-                    return service.munge_resp_data(final_resp)
+                    return service.munge_resp_data(final_resp,'checkout',method)
                 })
             });
         });
@@ -171,7 +174,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog) {
                     return service.handle_renew_resp(evt, params, options);
                 })
                 .then(function(final_resp) {
-                    return service.munge_resp_data(final_resp)
+                    return service.munge_resp_data(final_resp,'renew',method)
                 })
             });
         });
@@ -210,14 +213,14 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog) {
                     return service.handle_checkin_resp(evt, params, options);
                 })
                 .then(function(final_resp) {
-                    return service.munge_resp_data(final_resp)
+                    return service.munge_resp_data(final_resp,'checkin',method)
                 })
             });
         });
     }
 
     // provide consistent formatting of the final response data
-    service.munge_resp_data = function(final_resp) {
+    service.munge_resp_data = function(final_resp,worklog_action,worklog_method) {
         var data = final_resp.data = {};
 
         if (!final_resp.evt[0]) return;
@@ -256,6 +259,19 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog) {
             }
         }
 
+        egWorkLog.record(
+            worklog_action == 'checkout'
+            ? egCore.strings.EG_WORK_LOG_CHECKOUT
+            : (worklog_action == 'renew'
+                ? egCore.strings.EG_WORK_LOG_RENEW
+                : egCore.strings.EG_WORK_LOG_CHECKIN // worklog_action == 'checkin'
+            ),{
+                'action' : worklog_action,
+                'method' : worklog_method,
+                'response' : final_resp
+            }
+        );
+
         return final_resp;
     }
 
diff --git a/Open-ILS/web/js/ui/default/staff/services/eframe.js b/Open-ILS/web/js/ui/default/staff/services/eframe.js
index ac54bd5..aa87c7b 100644
--- a/Open-ILS/web/js/ui/default/staff/services/eframe.js
+++ b/Open-ILS/web/js/ui/default/staff/services/eframe.js
@@ -244,6 +244,7 @@ angular.module('egCoreMod')
                 if ($scope.handlers) {
                     $scope.handlers.reload = $scope.reload;
                     angular.forEach($scope.handlers, function(val, key) {
+                        console.log('eframe applying xulG handlers: ' + key);
                         $scope.iframe.contentWindow.xulG[key] = val;
                     });
                 }
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 e5eb40f..b9b3a8e 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -484,3 +484,119 @@ function($window , egStrings) {
         };
     }
 ])
+
+.factory('egWorkLog', ['egCore', function(egCore) {
+    var service = {};
+
+    service.retrieve_all = function() {
+        var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
+        var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
+
+        return { 'work_log' : workLog, 'patron_log' : patronLog };
+    }
+
+    service.record = function(message,data) {
+        var max_entries;
+        var max_patrons;
+        if (typeof egCore != 'undefined') {
+            if (typeof egCore.env != 'undefined') {
+                if (typeof egCore.env.aous != 'undefined') {
+                    max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
+                    max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
+                } else {
+                    console.log('worklog: missing egCore.env.aous');
+                }
+            } else {
+                console.log('worklog: missing egCore.env');
+            }
+        } else {
+            console.log('worklog: missing egCore');
+        }
+        if (!max_entries) {
+            if (typeof egCore.org != 'undefined') {
+                if (typeof egCore.org.cachedSettings != 'undefined') {
+                    max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
+                } else {
+                    console.log('worklog: missing egCore.org.cachedSettings');
+                }
+            } else {
+                console.log('worklog: missing egCore.org');
+            }
+        }
+        if (!max_patrons) {
+            if (typeof egCore.org != 'undefined') {
+                if (typeof egCore.org.cachedSettings != 'undefined') {
+                    max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
+                } else {
+                    console.log('worklog: missing egCore.org.cachedSettings');
+                }
+            } else {
+                console.log('worklog: missing egCore.org');
+            }
+        }
+        if (!max_entries) {
+            max_entries = 20;
+            console.log('worklog: defaulting to max_entries = ' + max_entries);
+        }
+        if (!max_patrons) {
+            max_patrons = 10;
+            console.log('worklog: defaulting to max_patrons = ' + max_patrons);
+        }
+
+        var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
+        var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
+        var entry = {
+            'when' : new Date(),
+            'msg' : message,
+            'data' : data,
+            'action' : data.action,
+            'actor' : egCore.auth.user().usrname()
+        };
+        if (data.action == 'checkin') {
+            entry['item'] = data.response.params.copy_barcode;
+            entry['user'] = data.response.data.au.family_name();
+            entry['item_id'] = data.response.data.acp.id();
+            entry['patron_id'] = data.response.data.au.id();
+        }
+        if (data.action == 'checkout') {
+            entry['item'] = data.response.params.copy_barcode;
+            entry['user'] = data.response.data.au.family_name();
+            entry['item_id'] = data.response.data.acp.id();
+            entry['patron_id'] = data.response.data.au.id();
+        }
+        if (data.action == 'renew') {
+            entry['item'] = data.response.params.copy_barcode;
+            entry['user'] = data.response.data.au.family_name();
+            entry['item_id'] = data.response.data.acp.id();
+            entry['patron_id'] = data.response.data.au.id();
+        }
+        if (data.action == 'requested_hold'
+            || data.action == 'edited_patron'
+            || data.action == 'registered_patron'
+            || data.action == 'paid_bill') {
+            entry['patron_id'] = data.patron_id;
+        }
+        if (data.action == 'paid_bill') {
+            entry['amount'] = data.total_amount;
+        }
+
+        workLog.push( entry );
+        if (workLog.length > max_entries) workLog.shift();
+        egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
+
+        if (entry['patron_id']) {
+            var temp = [];
+            for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
+                if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
+            }
+            temp.push( entry );
+            if (temp.length > max_patrons) temp.shift();
+            patronLog = temp;
+            egCore.hatch.setLocalItem('eg.patron_log',patronLog);
+        }
+
+        console.log('worklog',entry);
+    }
+
+    return service;
+}]);

commit 4e1a8f8245d649e8574b0357f5e79f92414bba3b
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu May 5 16:35:25 2016 -0400

    webstaff: transit list: improve styling of form
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/transits/t_list.tt2 b/Open-ILS/src/templates/staff/circ/transits/t_list.tt2
index 47b9b01..34b19c4 100644
--- a/Open-ILS/src/templates/staff/circ/transits/t_list.tt2
+++ b/Open-ILS/src/templates/staff/circ/transits/t_list.tt2
@@ -4,13 +4,24 @@
   </div>
 </div>
 
-<span><label><input type="radio" ng-model="transit_direction" value="to"/>[% l('Transits To') %]</label></span>
-<span><label><input type="radio" ng-model="transit_direction" value="from"/>[% l('Transits From') %]</label></span>
-<br/>
-<span><label>[% l('Library: ' ) %]</label></span>
-<span><eg-org-selector selected="context_org"></eg-org-selector></span>
-<span><label>[% l('Start Date: ') %]</label><input eg-date-input ng-model="dates.start_date" /></span>
-<span><label>[% l('End Date: ') %]</label><input eg-date-input ng-model="dates.end_date" /></span>
+<div>
+  <div class="form-group col-md-4">
+    <div class="row">
+      <label><input type="radio" ng-model="transit_direction" value="to"></input>[% l('Transits To') %]</label>
+      <label><input type="radio" ng-model="transit_direction" value="from"></input>[% l('Transits From') %]</label>
+    </div>
+    <div class="row">
+      <label for="select-transit-ou">[% l('Library: ' ) %]</label>
+      <eg-org-selector id="select-transit-ou" selected="context_org"></eg-org-selector>
+    </div>
+  </div>
+  <div class="form-group form-inline col-md-4">
+    <label for="select-start-date">[% l('Start Date: ') %]</label>
+    <eg-date-input id="select-start-date" ng-model="dates.start_date"></eg-date-input>
+    <label for="select-end-date">[% l('End Date: ') %]</label>
+    <eg-date-input id="select-end-date" ng-model="dates.end_date"></eg-date-input>
+  </div>
+</div>
 
 <hr/>
 

commit 7482a039798fb9f71a64562b4a94d05be4db0ff8
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Wed May 4 15:01:50 2016 -0400

    webstaff: transit list: don't reset form
    
    $location.path(foo) is a setter, and should not
    be used to calculate a URL for opening a new window
    unless you also want to refresh the source page.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/web/js/ui/default/staff/circ/transits/list.js b/Open-ILS/web/js/ui/default/staff/circ/transits/list.js
index b3634c4..9dd066d 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/transits/list.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/transits/list.js
@@ -18,8 +18,8 @@ angular.module('egTransitListApp',
 })
 
 .controller('TransitListCtrl',
-       ['$scope','$q','$routeParams','$window','$location','egCore','egTransits','egGridDataProvider',
-function($scope , $q , $routeParams , $window , $location , egCore , egTransits , egGridDataProvider) {
+       ['$scope','$q','$routeParams','$window','egCore','egTransits','egGridDataProvider',
+function($scope , $q , $routeParams , $window , egCore , egTransits , egGridDataProvider) {
 
     var transits = [];
     var provider = egGridDataProvider.instance({});
@@ -68,11 +68,10 @@ function($scope , $q , $routeParams , $window , $location , egCore , egTransits
         if (!angular.isArray(transits)) transits = [transits];
         angular.forEach(transits, function(transit) {
             $window.open(
-                $location.path(
-                    '/cat/item/' + transit.target_copy().id()
-                ).absUrl(),
+                egCore.env.basePath + '/cat/item/' +
+                transit.target_copy().id(),
                 '_blank'
-            ).focus();
+            ).focus()
         });
     }
 

commit bc5aebd7b20b53b24fa2c5c7eb066d7d21cb3689
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Thu Oct 1 07:23:44 2015 -0400

    webstaff: Transit List
    
    under Administration -> Local Administration
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/share/t_abort_transit_dialog.tt2 b/Open-ILS/src/templates/staff/circ/share/t_abort_transit_dialog.tt2
new file mode 100644
index 0000000..7490e53
--- /dev/null
+++ b/Open-ILS/src/templates/staff/circ/share/t_abort_transit_dialog.tt2
@@ -0,0 +1,32 @@
+<form ng-submit="ok()" role="form" class="form-horizontal">
+  <div class="modal-content">
+    <div class="modal-header">
+      <button type="button" class="close" 
+        ng-click="cancel($event)" aria-hidden="true">×</button>
+      <h4 class="modal-title">
+        [% l('Abort Transits and Reset Associated Holds') %]
+      </h4>
+    </div>
+    <div class="modal-body">
+      <span class="form-group">
+        <ng-pluralize count="num_transits"
+            when="{
+                'one': '[% l('Abort 1 transit?') %]',
+                'other' : '[% l('Abort [_1] transits?','{{num_transits}}') %]'
+            }">
+        </ng-pluralize>
+        <ng-pluralize count="num_hold_transits"
+            when="{
+                'one' : '[% l('There is 1 associated hold.') %]',
+                'other' : '[%l ('There are [_1] associated holds.','{{num_hold_transits}}') %]'
+            }">
+        </ng-pluralize>
+      </span>
+    </div>
+    <div class="modal-footer">
+      <input type="submit" class="btn btn-success" value="[% l('Abort Transit') %]"/>
+      <button class="btn btn-warning" ng-click="cancel($event)">[% l('Exit') %]</button>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/templates/staff/circ/transits/list.tt2 b/Open-ILS/src/templates/staff/circ/transits/list.tt2
new file mode 100644
index 0000000..950a57e
--- /dev/null
+++ b/Open-ILS/src/templates/staff/circ/transits/list.tt2
@@ -0,0 +1,18 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Transit List"); 
+  ctx.page_app = "egTransitListApp";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/transits.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/transits/list.js"></script>
+<link rel="stylesheet" href="[% ctx.base_path %]/staff/css/circ.css" />
+[% END %]
+
+<div ng-view></div>
+
+[% END %]
diff --git a/Open-ILS/src/templates/staff/circ/transits/t_list.tt2 b/Open-ILS/src/templates/staff/circ/transits/t_list.tt2
new file mode 100644
index 0000000..47b9b01
--- /dev/null
+++ b/Open-ILS/src/templates/staff/circ/transits/t_list.tt2
@@ -0,0 +1,50 @@
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    <span>[% l('Transit List') %]</span>
+  </div>
+</div>
+
+<span><label><input type="radio" ng-model="transit_direction" value="to"/>[% l('Transits To') %]</label></span>
+<span><label><input type="radio" ng-model="transit_direction" value="from"/>[% l('Transits From') %]</label></span>
+<br/>
+<span><label>[% l('Library: ' ) %]</label></span>
+<span><eg-org-selector selected="context_org"></eg-org-selector></span>
+<span><label>[% l('Start Date: ') %]</label><input eg-date-input ng-model="dates.start_date" /></span>
+<span><label>[% l('End Date: ') %]</label><input eg-date-input ng-model="dates.end_date" /></span>
+
+<hr/>
+
+<eg-grid
+  id-field="id"
+  idl-class="atc"
+  features="-sort,-multisort"
+  items-provider="grid_data_provider"
+  grid-controls="grid_controls"
+  persist-key="circ.transits.list"
+>
+
+  <eg-grid-menu-item handler="load_item" 
+    label="[% l('Item Status') %]"></eg-grid-menu-item>
+
+  <eg-grid-menu-item handler="abort_transit" 
+    label="[% l('Abort Transit') %]"></eg-grid-menu-item>
+
+  <eg-grid-field path='id' hidden></eg-grid-field>
+  <eg-grid-field path='target_copy.barcode'></eg-grid-field>
+  <eg-grid-field path='target_copy.circ_lib.shortname' hidden></eg-grid-field>
+  <eg-grid-field path='target_copy.call_number.label' hidden></eg-grid-field>
+  <eg-grid-field path='target_copy.call_number.record.simple_record.title'></eg-grid-field>
+  <eg-grid-field path='target_copy.call_number.record.simple_record.author' hidden></eg-grid-field>
+  <eg-grid-field path='source.shortname' label="[% l('Source Library') %]"></eg-grid-field>
+  <eg-grid-field path='dest.shortname' label="[% l('Destination Library') %]"></eg-grid-field>
+  <eg-grid-field path='source_send_time'></eg-grid-field>
+  <eg-grid-field path='dest_recv_time'></eg-grid-field>
+  <eg-grid-field path='hold_transit_copy.hold.hold_type'></eg-grid-field>
+  <eg-grid-field path='hold_transit_copy.hold.request_time' hidden></eg-grid-field>
+  <eg-grid-field path='hold_transit_copy.hold.capture_time' hidden></eg-grid-field>
+  <eg-grid-field path='hold_transit_copy.hold.expire_time' hidden></eg-grid-field>
+  <eg-grid-field path='hold_transit_copy.hold.usr.family_name' hidden></eg-grid-field>
+  <eg-grid-field path='hold_transit_copy.hold.usr.first_given_name' hidden></eg-grid-field>
+  <eg-grid-field path='hold_transit_copy.hold.usr.card.barcode' label="[% l('Patron Barcode') %]" hidden></eg-grid-field>
+</eg-grid>
+
diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/transits.js b/Open-ILS/web/js/ui/default/staff/circ/services/transits.js
new file mode 100644
index 0000000..32781a1
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/transits.js
@@ -0,0 +1,68 @@
+/**
+ * Transits, yo
+ */
+
+angular.module('egCoreMod')
+
+.factory('egTransits',
+
+       ['$uibModal','$q','egCore','egConfirmDialog','egAlertDialog',
+function($uibModal , $q , egCore , egConfirmDialog , egAlertDialog) {
+
+    var service = {};
+
+    service.abort_transits = function(transits,callback) {
+       
+        return $uibModal.open({
+            templateUrl : './circ/share/t_abort_transit_dialog',
+            controller : 
+                ['$scope', '$uibModalInstance',
+                function($scope, $uibModalInstance) {
+
+                    $scope.num_transits = transits.length;
+                    $scope.num_hold_transits = 0;
+                    angular.forEach(transits, function(t) {
+                        if (t.hold_transit_copy()) {
+                            $scope.num_hold_transits++;
+                        }
+                    });
+                    
+                    $scope.cancel = function($event) {
+                        $uibModalInstance.dismiss();
+                        $event.preventDefault();
+                    }
+
+                    $scope.ok = function() {
+
+                        function abort_one() {
+                            var transit = transits.pop();
+                            if (!transit) {
+                                $uibModalInstance.close();
+                                return;
+                            }
+                            egCore.net.request(
+                                'open-ils.circ', 'open-ils.circ.transit.abort',
+                                egCore.auth.token(), { 'transitid' : transit.id() }
+                            ).then(function(resp) {
+                                if (evt = egCore.evt.parse(resp)) {
+                                    console.error('unable to abort transit: ' 
+                                        + evt.toString());
+                                }
+                                abort_one();
+                            });
+                        }
+
+                        abort_one();
+                    }
+                }
+            ]
+        }).result.then(
+            function() {
+                callback();
+            }
+        );
+    }
+
+    return service;
+}])
+;
diff --git a/Open-ILS/web/js/ui/default/staff/circ/transits/list.js b/Open-ILS/web/js/ui/default/staff/circ/transits/list.js
new file mode 100644
index 0000000..b3634c4
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/circ/transits/list.js
@@ -0,0 +1,154 @@
+angular.module('egTransitListApp', 
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+    $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
+
+    var resolver = {delay : 
+        ['egStartup', function(egStartup) {return egStartup.go()}]}
+
+    $routeProvider.when('/circ/transits/list', {
+        templateUrl: './circ/transits/t_list',
+        controller: 'TransitListCtrl',
+        resolve : resolver
+    });
+
+    $routeProvider.otherwise({redirectTo : '/circ/transits/list'});
+})
+
+.controller('TransitListCtrl',
+       ['$scope','$q','$routeParams','$window','$location','egCore','egTransits','egGridDataProvider',
+function($scope , $q , $routeParams , $window , $location , egCore , egTransits , egGridDataProvider) {
+
+    var transits = [];
+    var provider = egGridDataProvider.instance({});
+    $scope.grid_data_provider = provider;
+    $scope.transit_direction = 'to';
+
+    function init_dates() {
+        // setup date filters
+        var start = new Date(); // midnight this morning
+        start.setHours(0);
+        start.setMinutes(0);
+        var end = new Date(); // near midnight tonight
+        end.setHours(23);
+        end.setMinutes(59);
+        $scope.dates = {
+            start_date : start,
+            end_date : new Date()
+        }
+    }
+    init_dates();
+
+    function date_range() {
+        if ($scope.dates.start_date > $scope.dates.end_date) {
+            var tmp = $scope.dates.start_date;
+            $scope.dates.start_date = $scope.dates.end_date;
+            $scope.dates.end_date = tmp;
+        }
+        $scope.dates.start_date.setHours(0);
+        $scope.dates.start_date.setMinutes(0);
+        $scope.dates.end_date.setHours(23);
+        $scope.dates.end_date.setMinutes(59);
+        try {
+            var start = $scope.dates.start_date.toISOString().replace(/T.*/,'');
+            var end = $scope.dates.end_date.toISOString().replace(/T.*/,'');
+        } catch(E) { // handling empty date widgets; maybe dangerous if something else can happen
+            init_dates();
+            return date_range();
+        }
+        var today = new Date().toISOString().replace(/T.*/,'');
+        if (end == today) end = 'now';
+        return [start, end];
+    }
+
+    function load_item(transits) {
+        if (!transits) return;
+        if (!angular.isArray(transits)) transits = [transits];
+        angular.forEach(transits, function(transit) {
+            $window.open(
+                $location.path(
+                    '/cat/item/' + transit.target_copy().id()
+                ).absUrl(),
+                '_blank'
+            ).focus();
+        });
+    }
+
+    $scope.load_item = function(action, data, transits) {
+        load_item(transits);
+    }
+
+    function abort_transit(transits) {
+        if (!transits) return;
+        if (!angular.isArray(transits)) transits = [transits];
+        if (transits.length == 0) return;
+        egTransits.abort_transits( transits, refresh_page );
+    }
+
+    $scope.abort_transit = function(action, date, transits) {
+        abort_transit(transits);
+    }
+
+    $scope.grid_controls = {
+        activateItem : load_item
+    }
+
+    function refresh_page() {
+        transits = [];
+        provider.refresh();
+    }
+
+    provider.get = function(offset, count) {
+        var deferred = $q.defer();
+        var recv_index = 0;
+
+        var filter = {
+            'source_send_time' : { 'between' : date_range() }
+        };
+        if ($scope.transit_direction == 'to') { filter['dest'] = $scope.context_org.id(); }
+        if ($scope.transit_direction == 'from') { filter['source'] = $scope.context_org.id(); }
+
+        egCore.pcrud.search('atc',
+            filter, {
+                'flesh' : 5,
+                // atc -> target_copy       -> call_number -> record -> simple_record
+                // atc -> hold_transit_copy -> hold        -> usr    -> card
+                'flesh_fields' : {
+                    'atc' : ['target_copy','dest','source','hold_transit_copy'],
+                    'acp' : ['call_number','location','circ_lib'],
+                    'acn' : ['record'],
+                    'bre' : ['simple_record'],
+                    'ahtc' : ['hold'],
+                    'ahr' : ['usr'],
+                    'au' : ['card']
+                },
+                'select' : { 'bre' : ['id'] }
+            }
+        ).then(
+            deferred.resolve, null, 
+            function(transit) {
+                transits[offset + recv_index++] = transit;
+                deferred.notify(transit);
+            }
+        );
+
+        return deferred.promise;
+    }
+
+    $scope.context_org = egCore.org.get(egCore.auth.user().ws_ou());
+    $scope.$watch('context_org', function(newVal, oldVal) {
+        if (newVal && newVal != oldVal) refresh_page();
+    });
+    $scope.$watch('transit_direction', function(newVal, oldVal) {
+        if (newVal && newVal != oldVal) refresh_page();
+    });
+    $scope.$watch('dates.start_date', function(newVal, oldVal) {
+        if (newVal && newVal != oldVal) refresh_page();
+    });
+    $scope.$watch('dates.end_date', function(newVal, oldVal) {
+        if (newVal && newVal != oldVal) refresh_page();
+    });
+}])
+

commit f3931174b5257fa1574b298c3608839bdcddaf12
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Tue Jan 5 11:23:36 2016 -0500

    webstaff: links to Surveys, Transit List, Work Log
    
    Under Administration -> Local Administration
    
    Links to the existing Surveys interface; following commits implement
    the new Transit List and Work Log interfaces.
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/admin/local/t_splash.tt2 b/Open-ILS/src/templates/staff/admin/local/t_splash.tt2
index f6b7da3..aba2290 100644
--- a/Open-ILS/src/templates/staff/admin/local/t_splash.tt2
+++ b/Open-ILS/src/templates/staff/admin/local/t_splash.tt2
@@ -32,6 +32,9 @@
     ,[ l('Standing Penalties'), "./admin/local/config/standing_penalty" ]
     ,[ l('Statistical Categories Editor'), "./admin/local/asset/stat_cat_editor" ]
     ,[ l('Statistical Popularity Badges'), "./admin/local/rating/badge" ]
+    ,[ l('Surveys'), "./admin/local/action/survey" ]
+    ,[ l('Transit List'), "./circ/transits/list" ]
+    ,[ l('Work Log'), "./admin/workstation/log" ]
    ];
 
    USE table(interfaces, rows=9);

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

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |   68 ++-
 Open-ILS/src/extras/install/Makefile.debian-jessie |    3 +
 Open-ILS/src/extras/install/Makefile.ubuntu-xenial |    3 +
 Open-ILS/src/perlmods/lib/OpenILS/WWW/IDL2js.pm    |    5 +
 .../src/templates/opac/parts/place_hold_result.tt2 |   18 +-
 .../templates/staff/admin/actor/org_unit/index.tt2 |   23 +
 .../staff/admin/actor/org_unit/t_addresses_tab.tt2 |    1 +
 .../staff/admin/actor/org_unit/t_hours_tab.tt2     |    1 +
 .../staff/admin/actor/org_unit/t_index.tt2         |   51 ++
 .../staff/admin/actor/org_unit/t_main_tab.tt2      |   86 +++
 .../src/templates/staff/admin/local/t_splash.tt2   |    3 +
 .../staff/admin/server/config/marc_field.tt2       |   53 ++
 .../src/templates/staff/admin/server/index.tt2     |   15 +
 .../src/templates/staff/admin/server/t_splash.tt2  |   79 +++
 .../src/templates/staff/admin/workstation/log.tt2  |   17 +
 .../templates/staff/admin/workstation/t_log.tt2    |   59 ++
 .../templates/staff/admin/workstation/t_splash.tt2 |   24 +
 Open-ILS/src/templates/staff/base_js.tt2           |   17 +
 .../src/templates/staff/cat/catalog/t_holdings.tt2 |    1 +
 .../templates/staff/cat/item/t_summary_pane.tt2    |    2 +
 .../src/templates/staff/cat/volcopy/t_edit.tt2     |    2 +-
 .../staff/circ/share/t_abort_transit_dialog.tt2    |   32 +
 .../src/templates/staff/circ/transits/list.tt2     |   18 +
 .../src/templates/staff/circ/transits/t_list.tt2   |   61 ++
 Open-ILS/src/templates/staff/css/admin.css.tt2     |    7 +
 .../xul => src/templates/staff/css}/reporter.css   |    0
 Open-ILS/src/templates/staff/css/style.css.tt2     |    8 +-
 Open-ILS/src/templates/staff/navbar.tt2            |    6 +
 Open-ILS/src/templates/staff/reporter/index.tt2    |   21 +
 .../staff/reporter/share/report_strings.tt2        |  180 ++++++
 .../templates/staff/reporter/t_edit_template.tt2   |  235 ++++++++
 Open-ILS/src/templates/staff/reporter/t_legacy.tt2 |    1 +
 .../src/templates/staff/share/t_confirm_dialog.tt2 |    2 +-
 Open-ILS/src/templates/staff/share/t_datetime.tt2  |    2 +-
 .../templates/staff/share/t_fm_record_editor.tt2   |   62 ++
 .../src/templates/staff/share/t_select_dialog.tt2  |   23 +
 Open-ILS/src/templates/staff/t_splash.tt2          |   19 +
 .../audio/notifications/error.wav}                 |  Bin 45404 -> 45404 bytes
 .../audio/notifications/info.wav}                  |  Bin 4758 -> 4758 bytes
 .../audio/notifications/success.wav}               |  Bin 6012 -> 6012 bytes
 .../audio/notifications/warning.wav}               |  Bin 67628 -> 67628 bytes
 Open-ILS/web/conify/global/actor/org_unit.html     |    7 +
 .../web/conify/global/permission/grp_tree.html     |   10 +-
 Open-ILS/web/js/ui/default/staff/Gruntfile.js      |   24 +
 .../ui/default/staff/admin/actor/org_unit/app.js   |  143 +++++
 .../web/js/ui/default/staff/admin/server/app.js    |   80 +++
 .../staff/admin/server/config/marc_field.js        |   88 +++
 .../js/ui/default/staff/admin/workstation/app.js   |   17 +
 .../js/ui/default/staff/admin/workstation/log.js   |  166 ++++++
 Open-ILS/web/js/ui/default/staff/app.js            |   16 +-
 Open-ILS/web/js/ui/default/staff/bower.json        |    5 +-
 .../web/js/ui/default/staff/cat/catalog/app.js     |   21 +
 Open-ILS/web/js/ui/default/staff/cat/item/app.js   |    2 +-
 .../js/ui/default/staff/cat/services/holdings.js   |    2 +-
 .../web/js/ui/default/staff/cat/volcopy/app.js     |    2 +-
 .../web/js/ui/default/staff/circ/patron/app.js     |    4 +-
 .../web/js/ui/default/staff/circ/patron/bills.js   |   22 +-
 .../web/js/ui/default/staff/circ/patron/holds.js   |   14 +-
 .../web/js/ui/default/staff/circ/patron/regctl.js  |   28 +-
 .../web/js/ui/default/staff/circ/services/circ.js  |   68 ++-
 .../web/js/ui/default/staff/circ/services/holds.js |    4 +
 .../js/ui/default/staff/circ/services/transits.js  |   69 +++
 .../web/js/ui/default/staff/circ/transits/list.js  |  153 +++++
 .../ui/default/staff/reporter/services/template.js |  440 ++++++++++++++
 .../js/ui/default/staff/reporter/template/app.js   |  600 ++++++++++++++++++++
 Open-ILS/web/js/ui/default/staff/services/audio.js |   78 +++
 Open-ILS/web/js/ui/default/staff/services/auth.js  |   24 +-
 Open-ILS/web/js/ui/default/staff/services/core.js  |    2 +-
 .../web/js/ui/default/staff/services/coresvc.js    |    7 +-
 .../web/js/ui/default/staff/services/eframe.js     |    1 +
 .../ui/default/staff/services/fm_record_editor.js  |  187 ++++++
 Open-ILS/web/js/ui/default/staff/services/grid.js  |    2 +
 Open-ILS/web/js/ui/default/staff/services/hatch.js |   88 +++-
 Open-ILS/web/js/ui/default/staff/services/idl.js   |   82 +++-
 Open-ILS/web/js/ui/default/staff/services/ui.js    |  212 +++++++-
 .../web/js/ui/default/staff/test/karma.conf.js     |    5 +-
 Open-ILS/web/opac/locale/en-US/lang.dtd            |    1 +
 Open-ILS/web/reports/oils_rpt_folder_window.js     |   29 +-
 Open-ILS/web/reports/oils_rpt_param_editor.js      |    3 +-
 Open-ILS/web/reports/oils_rpt_utils.js             |    3 +
 Open-ILS/xsl/fm_IDL2js.xsl                         |    2 +-
 .../xul/staff_client/server/cat/copy_browser.js    |    1 +
 .../server/circ/alternate_copy_summary.js          |    7 +-
 .../server/circ/alternate_copy_summary.xul         |    3 +-
 Open-ILS/xul/staff_client/server/circ/util.js      |   15 +
 .../server/locale/en-US/circ.properties            |    1 +
 .../server/locale/en-US/common.properties          |    1 +
 87 files changed, 3823 insertions(+), 124 deletions(-)
 create mode 100644 Open-ILS/src/templates/staff/admin/actor/org_unit/index.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/actor/org_unit/t_addresses_tab.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/actor/org_unit/t_hours_tab.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/actor/org_unit/t_index.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/server/config/marc_field.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/server/index.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/server/t_splash.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/workstation/log.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/workstation/t_log.tt2
 create mode 100644 Open-ILS/src/templates/staff/circ/share/t_abort_transit_dialog.tt2
 create mode 100644 Open-ILS/src/templates/staff/circ/transits/list.tt2
 create mode 100644 Open-ILS/src/templates/staff/circ/transits/t_list.tt2
 copy Open-ILS/{web/reports/xul => src/templates/staff/css}/reporter.css (100%)
 create mode 100644 Open-ILS/src/templates/staff/reporter/index.tt2
 create mode 100644 Open-ILS/src/templates/staff/reporter/share/report_strings.tt2
 create mode 100644 Open-ILS/src/templates/staff/reporter/t_edit_template.tt2
 create mode 100644 Open-ILS/src/templates/staff/reporter/t_legacy.tt2
 create mode 100644 Open-ILS/src/templates/staff/share/t_fm_record_editor.tt2
 create mode 100644 Open-ILS/src/templates/staff/share/t_select_dialog.tt2
 copy Open-ILS/{xul/staff_client/server/skin/media/audio/redalert.wav => web/audio/notifications/error.wav} (100%)
 copy Open-ILS/{xul/staff_client/server/skin/media/audio/toggled.wav => web/audio/notifications/info.wav} (100%)
 copy Open-ILS/{xul/staff_client/server/skin/media/audio/bonus.wav => web/audio/notifications/success.wav} (100%)
 copy Open-ILS/{xul/staff_client/server/skin/media/audio/question.wav => web/audio/notifications/warning.wav} (100%)
 create mode 100644 Open-ILS/web/js/ui/default/staff/admin/actor/org_unit/app.js
 create mode 100644 Open-ILS/web/js/ui/default/staff/admin/server/app.js
 create mode 100644 Open-ILS/web/js/ui/default/staff/admin/server/config/marc_field.js
 create mode 100644 Open-ILS/web/js/ui/default/staff/admin/workstation/log.js
 create mode 100644 Open-ILS/web/js/ui/default/staff/circ/services/transits.js
 create mode 100644 Open-ILS/web/js/ui/default/staff/circ/transits/list.js
 create mode 100644 Open-ILS/web/js/ui/default/staff/reporter/services/template.js
 create mode 100644 Open-ILS/web/js/ui/default/staff/reporter/template/app.js
 create mode 100644 Open-ILS/web/js/ui/default/staff/services/audio.js
 create mode 100644 Open-ILS/web/js/ui/default/staff/services/fm_record_editor.js


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list