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

Evergreen Git git at git.evergreen-ils.org
Thu Feb 25 17:49:46 EST 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  db8bd918412d9ee7fe4f8928d3d85bc24aa5120e (commit)
       via  68dd6ce59f7eb48dc76a28689c507da321cbd62a (commit)
       via  3263fdac5bed41998dffc31df0b814f8e3b3505e (commit)
       via  3ae7902b43152aff7803a21460a6371510c25a45 (commit)
       via  3d21fded8fa5089aafe9618c4092f70738677d69 (commit)
       via  0d1486376bd8209fbef5e7279ea455fcf7cd2f91 (commit)
       via  87215054288fb4c23039f08f02a81e218ab29e4a (commit)
       via  4ee999b8f89d2a41cac194e94b827b3085b5d876 (commit)
       via  61d2c5016214902a48280b6b3e58126237ca304d (commit)
       via  12477de2c98c3ea45b35577a6892003f1341fb9d (commit)
       via  58d0590e07d3f4db68c9a6ae8b90a6f4f48aa4c4 (commit)
       via  6343de1a223951b00d613326fc95aa8eb5a274c4 (commit)
       via  34353e9714c9a619449ff5d287041e7f4f550f30 (commit)
       via  a05d0181b384f5716a88b4d2b19c693d1f3fc0ec (commit)
       via  cdab4c76d7f8b077aa838b1b9208fb4e920c7aeb (commit)
       via  94ed2db118a8e66ef2d82f305c3017075574a45c (commit)
       via  403a9371a2bb5f73d5c48c71bd4327842ecc250b (commit)
       via  d69bb376a59fd8dc2db95a26bccbc599c54defc7 (commit)
       via  0c355ea9d8f5420efa138cd7b279fb1874ea265a (commit)
       via  a5d5d19bd7dac767fc37f272858c286f6acf37a8 (commit)
       via  ade2612ee97f51588aaa848035e7a1ad700d9540 (commit)
       via  eb1ceb99bc814437c3ce402a5fad279113308341 (commit)
       via  cf7766e55838f73e8c6538e1deddc84ee46ea0eb (commit)
       via  2dedeb342e9e84563d345e498b26bb6d22138095 (commit)
       via  ae707cf1753728007e4f6811240e89cac9a9941c (commit)
       via  5ad63ee9e7c6490cc6600b5a1f1e32e373694518 (commit)
       via  70b83d975b8d84e075d8e31858383e7f3d967f37 (commit)
       via  b284be7f2408d7d2682db280b6e1f9eb0a366ff8 (commit)
       via  bb0c48a7e27e1b59cf5bd6ab3f2c10ae4d03f04a (commit)
       via  9d0fa75d93e732f89c175ee6bd19e7a301f06bf9 (commit)
       via  6319263f8356e5095c6ace3e36d9f33f2f79b1da (commit)
       via  5587c8cd022fb5b83c19446686385699f97b722e (commit)
       via  3a5cd43d7b9b320b300bc33c7bdc37ded291b58f (commit)
       via  425a295fdcd5210f1f313e1491f6e6f0885bcc97 (commit)
       via  d40fb3479d730662bf6bf37d4401a175a01054a6 (commit)
       via  71e317777d9def5446c37d707ba6c42b106779fa (commit)
       via  c273f651551d5b84a3efebb7978601295e2bec84 (commit)
       via  340dd74e9f387207331cfc7c5941d76af4816f1d (commit)
       via  04499c1ef890839f306b9e1803b25992751cd5cc (commit)
       via  04606618b40ef10d95e15393c17c4added059579 (commit)
       via  6882b669c93732bf791320d608ea8081002490c0 (commit)
       via  d61dcd4c8018b3784cfe9d1a791f9e6be841b168 (commit)
       via  81248da60ffd29993a9352c9bc8ee5c1bea488d4 (commit)
       via  1ef95797c8ad96456a715b12f82b73c9d94982fc (commit)
       via  ced2598e8f7957591a8573b11b21a46aaf31e377 (commit)
       via  c1134c519e644fe6a0d8bba788f93d3cb79a7025 (commit)
       via  e8014dcef616738ac8bac221bc52cf0307b31153 (commit)
       via  2279864bf129510c2502a8898a693fadc81ada2b (commit)
       via  89369b785a9933caf8128aacafb33670e6a7a249 (commit)
       via  b2834c2cd2e2f033fa36d8d4bf68751f5b063cf9 (commit)
       via  db936941e578a32ec4a12d5f4a33575ed262b571 (commit)
       via  eb48b3a3ce62807ea1f0a74220e2242c6ef47225 (commit)
       via  ea9fa67caf9af12321ceb6c29ea9942895ce4608 (commit)
      from  54f8888496d825310ce5b349d18753e2bbf0b6bf (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 db8bd918412d9ee7fe4f8928d3d85bc24aa5120e
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu Feb 25 17:48:54 2016 -0500

    LP#1452950: add release notes
    
    Currently treating this as a technology preview; this may
    change before release of 2.10.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/native_web_staff_client_patron_editor.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/native_web_staff_client_patron_editor.adoc
new file mode 100644
index 0000000..99c5f62
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Circulation/native_web_staff_client_patron_editor.adoc
@@ -0,0 +1,13 @@
+Web staff client patron editor
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The web staff interface now includes a patron editor/registration form
+that is written using AngularJS, leading to faster and more responsive
+patron editing.  This feature is currently available in preview mode, but
+supports the following actions:
+
+  * adding and editing base patron records and addresses
+  * setting statistical categories
+  * editing secondary groups
+  * cloning patron records
+  * duplicate detection
+  * surveys

commit 68dd6ce59f7eb48dc76a28689c507da321cbd62a
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Feb 21 18:43:53 2016 -0500

    LP#1452950 Patron reg pending users
    
    Support loading pending users and removing pending users after save.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/register.tt2 b/Open-ILS/src/templates/staff/circ/patron/register.tt2
index 834b68c..81b4279 100644
--- a/Open-ILS/src/templates/staff/circ/patron/register.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/register.tt2
@@ -6,7 +6,6 @@
 
 [% 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/services/user.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/date.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/register.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/regctl.js"></script>
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 4326cb5..c1e769e 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -22,41 +22,41 @@
 
 <div id="reg-alert-pane">
 
-	<div id="reg-dupe-links">
-		[%# dupe_search_encoded is uri escaped in the JS %]
-		<div class="alert alert-danger" ng-show="dupe_counts.name">
-			<a target="_blank"
-				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}">
-			[% l('[_1] patron(s) with same name', '{{dupe_counts.name}}') %]
-			</a>
-		</div>
-		<div class="alert alert-danger" ng-show="dupe_counts.email">
-			<a target="_blank"
-				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}">
-				[% l('[_1] patron(s) with same email', 
-				'{{dupe_counts.email}}') %]</a>
-		</div>
-		<div class="alert alert-danger" ng-show="dupe_counts.ident">
-			<a target="_blank" 
-				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}">
-				[% l('[_1] patron(s) with same identification', 
-				'{{dupe_counts.ident}}') %]</a>
-		</div>
-		<div class="alert alert-danger" ng-show="dupe_counts.phone">
-			<a target="_blank" 
-				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}">
-				[% l('[_1] patron(s) with same phone', 
-				'{{dupe_counts.phone}}') %]</a>
-		</div>
-		<div class="alert alert-danger" ng-show="dupe_counts.address">
-			<a target="_blank" 
-				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
-				[% l('[_1] patron(s) with same address', 
-				'{{dupe_counts.address}}') %]</a>
-		</div>
-	</div>
-
-	<!-- IDL field documentation window -->
+  <div id="reg-dupe-links">
+    [%# dupe_search_encoded is uri escaped in the JS %]
+    <div class="alert alert-danger" ng-show="dupe_counts.name">
+      <a target="_blank"
+        href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}">
+      [% l('[_1] patron(s) with same name', '{{dupe_counts.name}}') %]
+      </a>
+    </div>
+    <div class="alert alert-danger" ng-show="dupe_counts.email">
+      <a target="_blank"
+        href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}">
+        [% l('[_1] patron(s) with same email', 
+        '{{dupe_counts.email}}') %]</a>
+    </div>
+    <div class="alert alert-danger" ng-show="dupe_counts.ident">
+      <a target="_blank" 
+        href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}">
+        [% l('[_1] patron(s) with same identification', 
+        '{{dupe_counts.ident}}') %]</a>
+    </div>
+    <div class="alert alert-danger" ng-show="dupe_counts.phone">
+      <a target="_blank" 
+        href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}">
+        [% l('[_1] patron(s) with same phone', 
+        '{{dupe_counts.phone}}') %]</a>
+    </div>
+    <div class="alert alert-danger" ng-show="dupe_counts.address">
+      <a target="_blank" 
+        href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+        [% l('[_1] patron(s) with same address', 
+        '{{dupe_counts.address}}') %]</a>
+    </div>
+  </div>
+
+  <!-- IDL field documentation window -->
   <div class="alert alert-info" ng-show="selected_field_doc">
     <fieldset id="reg-field-doc">
       <legend>
@@ -65,6 +65,13 @@
       <div>{{selected_field_doc.string()}}</div>
     </fieldset>
   </div>
+
+  <div class="alert alert-info" ng-show="stage_user_requestor">
+    <a target="_blank" 
+      href="/eg/staff/circ/patron/{{stage_user.reqesting_usr()}}/edit">
+      [% l('Requested by [_1]', '{{stage_user_requestor}}') %]
+    </a>
+  </div>
 </div>
 
 
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 a1bd339..1e78455 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
@@ -42,6 +42,7 @@ angular.module('egCoreMod')
             service.get_stat_cats(),
             service.get_surveys(),
             service.get_clone_user(),
+            service.get_stage_user(),
             service.get_net_access_levels()
         ]);
     };
@@ -66,6 +67,48 @@ angular.module('egCoreMod')
         });
     }
 
+    service.get_stage_user = function() {
+        if (!service.stage_username) return $q.when();
+
+        // fetch the staged user object
+        return egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.stage.retrieve.by_username',
+            egCore.auth.token(), 
+            service.stage_username
+        ).then(function(suser) {
+            if (e = egCore.evt.parse(suser)) {
+                alert(e);
+            } else {
+                service.stage_user = suser;
+            }
+        }).then(function() {
+
+            if (!service.stage_user) return;
+            var requestor = service.stage_user.user.requesting_usr();
+
+            if (!requestor) return;
+
+            // fetch the requesting user
+            return egCore.net.request(
+                'open-ils.actor', 
+                'open-ils.actor.user.retrieve.parts',
+                egCore.auth.token(),
+                requestor, 
+                ['family_name', 'first_given_name', 'second_given_name'] 
+            ).then(function(parts) {
+                service.stage_user_requestor = 
+                    service.format_name(parts[0], parts[1], parts[2]);
+            })
+        });
+    }
+
+    // See note above about not loading egUser.
+    // TODO: i18n
+    service.format_name = function(last, first, middle) {
+        return last + ', ' + first + (middle ? ' ' + middle : '');
+    }
+
     //service.check_grp_app_perm = function(grp_id) {
 
     // determine which user groups our user is not allowed to modify
@@ -491,8 +534,7 @@ angular.module('egCoreMod')
 
         patron.home_ou = egCore.org.get(patron.home_ou.id);
         patron.expire_date = new Date(Date.parse(patron.expire_date));
-        patron.dob = patron.dob ?
-            new Date(Date.parse(patron.dob)) : null;
+        patron.dob = service.parse_dob(patron.dob);
         patron.profile = current.profile(); // pre-hash version
         patron.net_access_level = current.net_access_level();
         patron.ident_type = current.ident_type();
@@ -554,9 +596,108 @@ angular.module('egCoreMod')
         if (service.clone_user)
             service.copy_clone_data(user);
 
+        if (service.stage_user)
+            service.copy_stage_data(user);
+
         return user;
     }
 
+    // dob is always YYYY-MM-DD
+    // Dates of birth do not contain timezone info, which can lead to
+    // inconcistent timezone handling, potentially representing
+    // different points in time, depending on the implementation.
+    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse
+    // See "Differences in assumed time zone"
+    // TODO: move this into egDate ?
+    service.parse_dob = function(dob) {
+        if (!dob) return null;
+        var parts = dob.split('-');
+        var d = new Date(); // always local time zone, yay.
+        d.setFullYear(parts[0]);
+        d.setMonth(parts[1]);
+        d.setDate(parts[2]);
+        return d;
+    }
+
+    service.copy_stage_data = function(user) {
+        var cuser = service.stage_user;
+
+        // copy the data into our new user object
+
+        for (var key in egCore.idl.classes.stgu.field_map) {
+            if (egCore.idl.classes.au.field_map[key] &&
+                !egCore.idl.classes.stgu.field_map[key].virtual) {
+                if (cuser.user[key]() !== null)
+                    user[key] = cuser.user[key]();
+            }
+        }
+
+        if (user.home_ou) user.home_ou = egCore.org.get(user.home_ou);
+        if (user.profile) user.profile = egCore.env.pgt.map[user.profile];
+        if (user.ident_type) 
+            user.ident_type = egCore.env.cit.map[user.ident_type];
+        user.dob = service.parse_dob(user.dob);
+
+        // Clear the usrname if it looks like a UUID
+        if (user.usrname.replace(/-/g,'').match(/[0-9a-f]{32}/)) 
+            user.usrname = '';
+
+        // Don't use stub address if we have one from the staged user.
+        if (cuser.mailing_addresses.length || cuser.billing_addresses.length)
+            user.addresses = [];
+
+        // is_mailing=false implies is_billing
+        function addr_from_stage(stage_addr) {
+            if (!stage_addr) return;
+            var cls = stage_addr.classname;
+
+            var addr = {
+                id : service.virt_id--,
+                usr : user.id,
+                isnew : true,
+                valid : true,
+                _is_mailing : cls == 'stgma',
+                _is_billing : cls == 'stgba'
+            };
+
+            user.mailing_address = addr;
+            user.addresses.push(addr);
+
+            for (var key in egCore.idl.classes[cls].field_map) {
+                if (egCore.idl.classes.aua.field_map[key] &&
+                    !egCore.idl.classes[cls].field_map[key].virtual) {
+                    if (stage_addr[key]() !== null)
+                        addr[key] = stage_addr[key]();
+                }
+            }
+        }
+
+        addr_from_stage(cuser.mailing_addresses[0]);
+        addr_from_stage(cuser.billing_addresses[0]);
+
+        if (user.addresses.length == 1) {
+            // If there is only one address, 
+            // use it as both mailing and billing.
+            var addr = user.addresses[0];
+            addr._is_mailing = addr._is_billing = true;
+            user.mailing_address = user.billing_address = addr;
+        }
+
+        if (cuser.cards.length) {
+            user.card = {
+                id : service.virt_id--,
+                barcode : cuser.cards[0].barcode(),
+                isnew : true,
+                active : true,
+                _primary : 'on'
+            };
+
+            user.cards.push(user.card);
+            if (user.usrname == '') 
+                user.usrname = card.barcode;
+        }
+    }
+
     // copy select values from the cloned user to the new user.
     // user is a hash
     service.copy_clone_data = function(user) {
@@ -564,6 +705,7 @@ angular.module('egCoreMod')
 
         // flesh the home org locally
         user.home_ou = egCore.org.get(clone_user.home_ou());
+        if (user.profile) user.profile = egCore.env.pgt.map[user.profile];
 
         if (!clone_user.billing_address() &&
             !clone_user.mailing_address())
@@ -635,12 +777,11 @@ angular.module('egCoreMod')
                 user.billing_address._is_billing = true;
                 user.addresses.push(user.billing_address);
                 user.billing_address._linked_owner_id = clone_user.id();
-                // TODO: see note above about egUser, which has its
-                // own name formatting function.
-                user.billing_address._linked_owner =
-                    clone_user.family_name() + ', ' + 
-                    clone_user.first_given_name() + ' ' + 
-                    clone_user.second_given_name();
+                user.billing_address._linked_owner = service.format_name(
+                    clone_user.family_name(),
+                    clone_user.first_given_name(),
+                    clone_user.second_given_name()
+                );
             }
 
             if (addr = clone_user.mailing_address()) {
@@ -654,12 +795,11 @@ angular.module('egCoreMod')
                     user.mailing_address._is_mailing = true;
                     user.addresses.push(user.mailing_address);
                     user.mailing_address._linked_owner_id = clone_user.id();
-                    // TODO: see note above about egUser, which has its
-                    // own name formatting function.
-                    user.mailing_address._linked_owner =
-                        clone_user.family_name() + ', ' + 
-                        clone_user.first_given_name() + ' ' + 
-                        clone_user.second_given_name();
+                    user.mailing_address._linked_owner = service.format_name(
+                        clone_user.family_name(),
+                        clone_user.first_given_name(),
+                        clone_user.second_given_name()
+                    );
                 }
             }
         }
@@ -767,6 +907,16 @@ angular.module('egCoreMod')
             egCore.auth.token(), patron);
     }
 
+    service.remove_staged_user = function() {
+        if (!service.stage_user) return $q.when();
+        return egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.stage.delete',
+            egCore.auth.token(),
+            service.stage_user.row_id()
+        );
+    }
+
     service.save_user_settings = function(new_user, user_settings) {
         // user_settings contains the values from the scope/form.
         // service.user_settings contain the values from page load time.
@@ -807,7 +957,8 @@ function PatronRegCtrl($scope, $routeParams,
 
     $scope.page_data_loaded = false;
     $scope.clone_id = patronRegSvc.clone_id = $routeParams.clone_id;
-    $scope.stage_username = $routeParams.stage_username;
+    $scope.stage_username = 
+        patronRegSvc.stage_username = $routeParams.stage_username;
     $scope.patron_id = 
         patronRegSvc.patron_id = $routeParams.edit_id || $routeParams.id;
 
@@ -838,10 +989,16 @@ function PatronRegCtrl($scope, $routeParams,
     // Apply default values for new patrons during initial registration
     // prs is shorthand for patronSvc
     function set_new_patron_defaults(prs) {
-        $scope.generate_password();
+        if (!$scope.patron.passwd) {
+            // passsword may originate from staged user.
+            $scope.generate_password();
+        }
         $scope.hold_notify_phone = true;
         $scope.hold_notify_email = true;
 
+        // staged users may be loaded w/ a profile.
+        $scope.set_expire_date();
+
         if (prs.org_settings['ui.patron.default_ident_type']) {
             // $scope.patron needs this field to be an object
             var id = prs.org_settings['ui.patron.default_ident_type'];
@@ -894,6 +1051,8 @@ function PatronRegCtrl($scope, $routeParams,
         $scope.surveys = prs.surveys;
         $scope.survey_responses = prs.survey_responses;
         $scope.stat_cat_entry_maps = prs.stat_cat_entry_maps;
+        $scope.stage_user = prs.stage_user;
+        $scope.stage_user_requestor = prs.stage_user_requestor;
 
         $scope.user_settings = prs.user_settings;
         // clone the user settings back into the patronRegSvc so
@@ -1258,6 +1417,7 @@ function PatronRegCtrl($scope, $routeParams,
         compress_hold_notify();
 
         var updated_user;
+
         patronRegSvc.save_user($scope.patron)
         .then(function(new_user) { 
             if (new_user && new_user.classname) {
@@ -1266,13 +1426,20 @@ function PatronRegCtrl($scope, $routeParams,
                     new_user, $scope.user_settings); 
             } else {
                 alert('Patron update failed. \n\n' + js2JSON(new_user));
-                return true; // ensure page reloads to reset
             }
-        }).then(function(keep_going) {
+
+        }).then(function() {
+
+            // only remove the staged user if the update succeeded.
+            if (updated_user) 
+                return patronRegSvc.remove_staged_user();
+
+        }).then(function() {
+
             // reloading the page means potentially losing some information
             // (e.g. last patron search), but is the only way to ensure all
             // components are properly updated to reflect the modified patron.
-            if (save_args.clone) {
+            if (updated_user && save_args.clone) {
                 // open a separate tab for registering a new 
                 // patron from our cloned data.
                 var url = 'https://' 
@@ -1281,6 +1448,7 @@ function PatronRegCtrl($scope, $routeParams,
                     + '/circ/patron/register/clone/' 
                     + updated_user.id();
                 $window.open(url, '_blank').focus();
+
             } else {
                 // reload the current page
                 $window.location.href = location.href;

commit 3263fdac5bed41998dffc31df0b814f8e3b3505e
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Feb 18 09:59:09 2016 -0500

    LP#1452950 Patron reg. clone user
    
    Implemented Save & Clone.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2 b/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
index a35b949..a533ba8 100644
--- a/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
@@ -27,7 +27,8 @@
       ng-click="edit_passthru.save()">[% l('Save') %]</button>
   </span>
   <span class="pad-all-min">
-    <button type="button" class="btn btn-default">[% l('Save & Clone') %]</button>
+    <button type="button" class="btn btn-default"
+      ng_click="edit_passthru.save({clone:true})">[% l('Save & Clone') %]</button>
   </span>
 </div>
 
diff --git a/Open-ILS/src/templates/staff/circ/patron/register.tt2 b/Open-ILS/src/templates/staff/circ/patron/register.tt2
index 81b4279..834b68c 100644
--- a/Open-ILS/src/templates/staff/circ/patron/register.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/register.tt2
@@ -6,6 +6,7 @@
 
 [% 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/services/user.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/date.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/register.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/regctl.js"></script>
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 39a9567..4326cb5 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -498,7 +498,15 @@
 
 <div ng-repeat="addr in patron.addresses">
   <div class="alert alert-success row" role="alert">
-      <div class="col-md-3">[% l('Address') %]</div>
+      <div class="col-md-3">
+        [% l('Address') %]
+        <div ng-show="addr._linked_owner">
+          (<a target="_blank"
+            href="/eg/staff/circ/patron/{{addr._linked_owner_id}}/edit">
+            [% l('Owned by [_1]', '{{addr._linked_owner}}') %]
+          </a>)
+        </div>
+      </div>
       <div class="col-md-3">
           <span class='pad-all-min'>
             [% l('Mailing') %] <input type='checkbox' 
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 1d28a3b..a1bd339 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
@@ -41,10 +41,31 @@ angular.module('egCoreMod')
             service.get_org_settings(),
             service.get_stat_cats(),
             service.get_surveys(),
+            service.get_clone_user(),
             service.get_net_access_levels()
         ]);
     };
 
+    service.get_clone_user = function() {
+        if (!service.clone_id) return $q.when();
+        // we could load egUser and use its get() function, but loading
+        // user.js into the standalone register UI would mean creating a
+        // new module, since egUser is not loaded into egCoreMod.  This
+        // is a lot simpler.
+        return egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.fleshed.retrieve',
+            egCore.auth.token(), service.clone_id, 
+            ['billing_address', 'mailing_address'])
+        .then(function(cuser) {
+            if (e = egCore.evt.parse(cuser)) {
+                alert(e);
+            } else {
+                service.clone_user = cuser;
+            }
+        });
+    }
+
     //service.check_grp_app_perm = function(grp_id) {
 
     // determine which user groups our user is not allowed to modify
@@ -521,16 +542,127 @@ angular.module('egCoreMod')
             _primary : 'on'
         };
 
-        return {
+        var user = {
             isnew : true,
             active : true,
             card : card,
             cards : [card],
             home_ou : egCore.org.get(egCore.auth.user().ws_ou()),
-                        
-            // TODO default profile group?
             addresses : [addr]
         };
+
+        if (service.clone_user)
+            service.copy_clone_data(user);
+
+        return user;
+    }
+
+    // copy select values from the cloned user to the new user.
+    // user is a hash
+    service.copy_clone_data = function(user) {
+        var clone_user = service.clone_user;
+
+        // flesh the home org locally
+        user.home_ou = egCore.org.get(clone_user.home_ou());
+
+        if (!clone_user.billing_address() &&
+            !clone_user.mailing_address())
+            return; // no addresses to copy or link
+
+        // if the cloned user has any addresses, we don't need 
+        // the stub address created in init_new_patron.
+        user.addresses = [];
+
+        var copy_addresses = 
+            service.org_settings['circ.patron_edit.clone.copy_address'];
+
+        var clone_fields = [
+            'day_phone',
+            'evening_phone',
+            'other_phone',
+            'usrgroup'
+        ]; 
+
+        angular.forEach(clone_fields, function(field) {
+            user[field] = clone_user[field]();
+        });
+
+        if (copy_addresses) {
+            var bill_addr, mail_addr;
+
+            // copy the billing and mailing addresses into new addresses
+            function clone_addr(addr) {
+                var new_addr = egCore.idl.toHash(addr);
+                new_addr.id = service.virt_id--;
+                new_addr.usr = user.id;
+                new_addr.isnew = true;
+                new_addr.valid = true;
+                user.addresses.push(new_addr);
+                return new_addr;
+            }
+
+            if (bill_addr = clone_user.billing_address()) {
+                var addr = clone_addr(bill_addr);
+                addr._is_billing = true;
+                user.billing_address = addr;
+            }
+
+            if (mail_addr = clone_user.mailing_address()) {
+
+                if (bill_addr && bill_addr.id() == mail_addr.id()) {
+                    user.mailing_address = user.billing_address;
+                    user.mailing_address._is_mailing = true;
+                } else {
+                    var addr = clone_addr(mail_addr);
+                    addr._is_mailing = true;
+                    user.mailing_address = addr;
+                }
+
+                if (!bill_addr) {
+                    // if there is no billing addr, use the mailing addr
+                    user.billing_address = user.mailing_address;
+                    user.billing_address._is_billing = true;
+                }
+            }
+
+
+        } else {
+
+            // link the billing and mailing addresses
+            var addr;
+            if (addr = clone_user.billing_address()) {
+                user.billing_address = egCore.idl.toHash(addr);
+                user.billing_address._is_billing = true;
+                user.addresses.push(user.billing_address);
+                user.billing_address._linked_owner_id = clone_user.id();
+                // TODO: see note above about egUser, which has its
+                // own name formatting function.
+                user.billing_address._linked_owner =
+                    clone_user.family_name() + ', ' + 
+                    clone_user.first_given_name() + ' ' + 
+                    clone_user.second_given_name();
+            }
+
+            if (addr = clone_user.mailing_address()) {
+                if (user.billing_address && 
+                    addr.id() == user.billing_address.id) {
+                    // mailing matches billing
+                    user.mailing_address = user.billing_address;
+                    user.mailing_address._is_mailing = true;
+                } else {
+                    user.mailing_address = egCore.idl.toHash(addr);
+                    user.mailing_address._is_mailing = true;
+                    user.addresses.push(user.mailing_address);
+                    user.mailing_address._linked_owner_id = clone_user.id();
+                    // TODO: see note above about egUser, which has its
+                    // own name formatting function.
+                    user.mailing_address._linked_owner =
+                        clone_user.family_name() + ', ' + 
+                        clone_user.first_given_name() + ' ' + 
+                        clone_user.second_given_name();
+                }
+            }
+        }
     }
 
     // translate the patron back into IDL form
@@ -674,7 +806,7 @@ function PatronRegCtrl($scope, $routeParams,
     $q, $modal, $window, egCore, patronSvc, patronRegSvc, egUnloadPrompt) {
 
     $scope.page_data_loaded = false;
-    $scope.clone_id = $routeParams.clone_id;
+    $scope.clone_id = patronRegSvc.clone_id = $routeParams.clone_id;
     $scope.stage_username = $routeParams.stage_username;
     $scope.patron_id = 
         patronRegSvc.patron_id = $routeParams.edit_id || $routeParams.id;
@@ -781,6 +913,7 @@ function PatronRegCtrl($scope, $routeParams,
             set_new_patron_defaults(prs);
 
         $scope.page_data_loaded = true;
+
     });
 
     // update the currently displayed field documentation
@@ -1111,7 +1244,8 @@ function PatronRegCtrl($scope, $routeParams,
         });
     }
 
-    $scope.edit_passthru.save = function() {
+    $scope.edit_passthru.save = function(save_args) {
+        if (!save_args) save_args = {};
 
         // remove page unload warning prompt
         egUnloadPrompt.clear();
@@ -1123,9 +1257,11 @@ function PatronRegCtrl($scope, $routeParams,
         
         compress_hold_notify();
 
+        var updated_user;
         patronRegSvc.save_user($scope.patron)
         .then(function(new_user) { 
             if (new_user && new_user.classname) {
+                updated_user = new_user;
                 return patronRegSvc.save_user_settings(
                     new_user, $scope.user_settings); 
             } else {
@@ -1136,7 +1272,19 @@ function PatronRegCtrl($scope, $routeParams,
             // 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.
-            $window.location.href = location.href;
+            if (save_args.clone) {
+                // open a separate tab for registering a new 
+                // patron from our cloned data.
+                var url = 'https://' 
+                    + $window.location.hostname 
+                    + egCore.env.basePath 
+                    + '/circ/patron/register/clone/' 
+                    + updated_user.id();
+                $window.open(url, '_blank').focus();
+            } else {
+                // reload the current page
+                $window.location.href = location.href;
+            }
         });
     }
 }

commit 3ae7902b43152aff7803a21460a6371510c25a45
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Feb 16 21:40:57 2016 -0500

    LP#1452950 date input supports null dates
    
    Leave a date unset when its value is null instead of defaulting to now.
    Support unsetting a date when its value is cleared.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 d9f77fa..95a60d6 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -487,10 +487,12 @@ It also allows us to abstract away some browser finickiness.
                 ngModelCtrl.$formatters.unshift(function (modelValue) {
                     // apply strip_time here in case the user never 
                     // modifies the date value.
+                    if (!modelValue) return '';
                     return dateFilter(strip_time(modelValue), 'yyyy-MM-dd');
                 });
                 
                 ngModelCtrl.$parsers.unshift(function(viewValue) {
+                    if (!viewValue) return null;
                     return strip_time(new Date(viewValue));
                 });
             },

commit 3d21fded8fa5089aafe9618c4092f70738677d69
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Feb 16 20:56:26 2016 -0500

    LP#1452950 re-attach unload warning after click-through
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 9385086..1d28a3b 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
@@ -695,10 +695,11 @@ function PatronRegCtrl($scope, $routeParams,
     $scope.edit_passthru.vis_level = 0; 
     // TODO: add save/clone handlers here
 
-    var modify_tracked = false;
     $scope.field_modified = function() {
-        if (modify_tracked) return;
-        modify_tracked = true;
+        // Call attach with every field change, regardless of whether
+        // it's been called before.  This will allow for re-attach after
+        // the user clicks through the unload warning. egUnloadPrompt
+        // will ensure we only attach once.
         egUnloadPrompt.attach($scope);
     }
 
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 7f84666..d9f77fa 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -195,10 +195,12 @@ function($modal, $interpolate) {
 .factory('egUnloadPrompt', [
         '$window','egStrings', 
 function($window , egStrings) {
-    var service = {};
+    var service = {attached : false};
 
     // attach a page/scope unload prompt
     service.attach = function($scope, msg) {
+        if (service.attached) return;
+        service.attached = true;
 
         // handle page change
         $($window).on('beforeunload', function() { 
@@ -227,6 +229,7 @@ function($window , egStrings) {
         $($window).off('beforeunload');
         if (service.locChangeCancel)
             service.locChangeCancel();
+        service.attached = false;
     }
 
     return service;

commit 0d1486376bd8209fbef5e7279ea455fcf7cd2f91
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sat Jan 23 15:19:00 2016 -0500

    LP#1452950 Support free-text stat cats
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 4a804f9..39a9567 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -549,22 +549,29 @@
     <label>{{cat.name()}}</label>
   </div>
   <div class="col-md-3 reg-field-input">
-    <div ng-if="cat.entries().length == 0">
-      <input 
-        ng-change="field_modified()" 
-        type="text" class="form-control"/>
-    </div>
+
+    <!-- Editable typeahead is not support in this version of 
+        angularjs-bootstrap.  Requires Angular 1.4 and 
+        angularjs-bootstrap version 1.1+ -->
+
+    <!-- typeahead is wonky.  Consider updating -->
+    <!--
+    <input type="text" ng-model="stat_cat_entry_maps[cat.id()]"
+      typeahead="value as entry.value() for entry in cat.entries() | filter:$viewValue | limitTo:8" 
+      class="form-control">
+    -->
+    
     <div ng-if="cat.entries().length != 0">
       <div class="btn-group" dropdown>
         <button type="button" class="btn btn-default dropdown-toggle">
           <span style="padding-right: 5px;">
-            {{stat_cat_entry_maps[cat.id()].value()}}</span>
+            {{stat_cat_entry_maps[cat.id()]}}</span>
           <span class="caret"></span>
         </button>
         <ul class="dropdown-menu">
           <li ng-repeat="entry in cat.entries()">
             <a href 
-              ng-click="field_modified();stat_cat_entry_maps[cat.id()]=entry"> 
+              ng-click="field_modified();stat_cat_entry_maps[cat.id()]=entry.value()"> 
               {{entry.value()}}
             </a>
           </li>
@@ -572,6 +579,12 @@
       </div>
     </div>
   </div>
+
+  <!-- Stat cat retrieval API uses open-ils.storage under the covers
+      which represents DB bools at 1/0 instead of cstore-style t/f -->
+  <div class="col-md-3 reg-field-input" ng-if="cat.allow_freetext() == '1'">
+    <input type="text" ng-model="stat_cat_entry_maps[cat.id()]"/>
+  </div>
 </div>
 
 <!-- surveys -->
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 8e128ec..9385086 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
@@ -17,7 +17,7 @@ angular.module('egCoreMod')
         survey_answers : {},
         survey_responses : {},     // survey.responses for loaded patron in progress
         stat_cats : [],
-        stat_cat_entry_maps : {},   // cat.id to selected entry object map
+        stat_cat_entry_maps : {},   // cat.id to selected value
         virt_id : -1,               // virtual ID for new objects
         init_done : false           // have we loaded our initialization data?
     };
@@ -496,14 +496,7 @@ angular.module('egCoreMod')
         // toss entries for existing stat cat maps into our living 
         // stat cat entry map, which is modified within the template.
         angular.forEach(patron.stat_cat_entries, function(map) {
-            var entry;
-            angular.forEach(service.stat_cats, function(cat) {
-                angular.forEach(cat.entries(), function(ent) {
-                    if (ent.value() == map.stat_cat_entry)
-                        entry = ent;
-                });
-            });
-            service.stat_cat_entry_maps[map.stat_cat.id] = entry;
+            service.stat_cat_entry_maps[map.stat_cat.id] = map.stat_cat_entry;
         });
 
         return patron;
@@ -607,19 +600,19 @@ angular.module('egCoreMod')
         });
         patron.stat_cat_entries(maps);
 
-        // service.stat_cat_entry_maps maps stats to entries
+        // service.stat_cat_entry_maps maps stats to values
         // patron.stat_cat_entries is an array of stat_cat_entry_usr_map's
-        angular.forEach(service.stat_cat_entry_maps, function(entry) {
+        angular.forEach(
+            service.stat_cat_entry_maps, function(value, cat_id) {
 
             // see if we already have a mapping for this entry
-            var existing = patron.stat_cat_entries().filter(function(e) {
-                return e.stat_cat() == entry.stat_cat();
-            })[0];
+            var existing = patron.stat_cat_entries().filter(
+                function(e) { return e.stat_cat() == cat_id })[0];
 
             if (existing) { // we have a mapping
                 // if the existing mapping matches the new one,
                 // there' nothing left to do
-                if (existing.stat_cat_entry() == entry.value()) return;
+                if (existing.stat_cat_entry() == value) return;
 
                 // mappings differ.  delete the old one and create
                 // a new one below.
@@ -629,15 +622,11 @@ angular.module('egCoreMod')
             var newmap = new egCore.idl.actscecm();
             newmap.target_usr(patron.id());
             newmap.isnew(true);
-            newmap.stat_cat(entry.stat_cat());
-            newmap.stat_cat_entry(entry.value());
+            newmap.stat_cat(cat_id);
+            newmap.stat_cat_entry(value);
             patron.stat_cat_entries().push(newmap);
         });
 
-        angular.forEach(patron.stat_cat_entries(), function(entry) {
-            console.log(egCore.idl.toString(entry));
-        });
-
         if (!patron.isnew()) patron.ischanged(true);
 
         return egCore.net.request(

commit 87215054288fb4c23039f08f02a81e218ab29e4a
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Jan 21 22:21:15 2016 -0500

    LP#1452950 Stat cat maps store values, not links
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 d9dc78f..8e128ec 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
@@ -499,7 +499,7 @@ angular.module('egCoreMod')
             var entry;
             angular.forEach(service.stat_cats, function(cat) {
                 angular.forEach(cat.entries(), function(ent) {
-                    if (ent.id() == map.stat_cat_entry)
+                    if (ent.value() == map.stat_cat_entry)
                         entry = ent;
                 });
             });
@@ -619,7 +619,7 @@ angular.module('egCoreMod')
             if (existing) { // we have a mapping
                 // if the existing mapping matches the new one,
                 // there' nothing left to do
-                if (existing.stat_cat_entry() == entry.id()) return;
+                if (existing.stat_cat_entry() == entry.value()) return;
 
                 // mappings differ.  delete the old one and create
                 // a new one below.
@@ -630,7 +630,7 @@ angular.module('egCoreMod')
             newmap.target_usr(patron.id());
             newmap.isnew(true);
             newmap.stat_cat(entry.stat_cat());
-            newmap.stat_cat_entry(entry.id());
+            newmap.stat_cat_entry(entry.value());
             patron.stat_cat_entries().push(newmap);
         });
 

commit 4ee999b8f89d2a41cac194e94b827b3085b5d876
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sat Jan 16 15:15:01 2016 -0500

    LP#1452950 Remove unsaved data warning after click-thru
    
    Once the user clicks through the unsaved data warning, clear the warning
    for future navigation.  If more fields are changed, the warning will be
    reinstated.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 2ffbc22..7f84666 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -202,6 +202,7 @@ function($window , egStrings) {
 
         // handle page change
         $($window).on('beforeunload', function() { 
+            service.clear();
             return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
         });
 
@@ -211,8 +212,13 @@ function($window , egStrings) {
         // similar to the page-page prompt.
         service.locChangeCancel = 
             $scope.$on('$locationChangeStart', function(evt, next, current) {
-            if (!confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) 
+            if (confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) {
+                // user allowed the page to change.  
+                // Clear the unload handler.
+                service.clear();
+            } else {
                 evt.preventDefault();
+            }
         });
     };
 

commit 61d2c5016214902a48280b6b3e58126237ca304d
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Dec 20 19:51:01 2015 -0500

    LP#1452950 Unload prompts more handlers / dupe styling
    
    Link in remaining unload onchange handlers.  Use bootstrap alert styles
    for dupe patron links.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 0879eb4..4a804f9 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -24,32 +24,32 @@
 
 	<div id="reg-dupe-links">
 		[%# dupe_search_encoded is uri escaped in the JS %]
-		<div>
-			<a target="_blank" ng-show="dupe_counts.name"
-				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+		<div class="alert alert-danger" ng-show="dupe_counts.name">
+			<a target="_blank"
+				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}">
 			[% l('[_1] patron(s) with same name', '{{dupe_counts.name}}') %]
 			</a>
 		</div>
-		<div>
-			<a target="_blank" ng-show="dupe_counts.email"
-				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+		<div class="alert alert-danger" ng-show="dupe_counts.email">
+			<a target="_blank"
+				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}">
 				[% l('[_1] patron(s) with same email', 
 				'{{dupe_counts.email}}') %]</a>
 		</div>
-		<div>
-			<a target="_blank" ng-show="dupe_counts.ident"
-				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+		<div class="alert alert-danger" ng-show="dupe_counts.ident">
+			<a target="_blank" 
+				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}">
 				[% l('[_1] patron(s) with same identification', 
 				'{{dupe_counts.ident}}') %]</a>
 		</div>
-		<div>
-			<a target="_blank" ng-show="dupe_counts.phone"
-				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+		<div class="alert alert-danger" ng-show="dupe_counts.phone">
+			<a target="_blank" 
+				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}">
 				[% l('[_1] patron(s) with same phone', 
 				'{{dupe_counts.phone}}') %]</a>
 		</div>
-		<div>
-			<a target="_blank" ng-show="dupe_counts.address"
+		<div class="alert alert-danger" ng-show="dupe_counts.address">
+			<a target="_blank" 
 				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
 				[% l('[_1] patron(s) with same address', 
 				'{{dupe_counts.address}}') %]</a>
@@ -57,12 +57,14 @@
 	</div>
 
 	<!-- IDL field documentation window -->
-	<fieldset id="reg-field-doc" ng-show="selected_field_doc">
-		<legend>
-		{{idl_fields[selected_field_doc.fm_class()][selected_field_doc.field()].label}}
-		</legend>
-		<div>{{selected_field_doc.string()}}</div>
-	</fieldset>
+  <div class="alert alert-info" ng-show="selected_field_doc">
+    <fieldset id="reg-field-doc">
+      <legend>
+      {{idl_fields[selected_field_doc.fm_class()][selected_field_doc.field()].label}}
+      </legend>
+      <div>{{selected_field_doc.string()}}</div>
+    </fieldset>
+  </div>
 </div>
 
 
@@ -99,48 +101,59 @@
     <!-- text / number input -->
 
     [% IF field == 'alert_message' %]
-      <textarea class="form-control" ng-model="[% model %]"/>
+      <textarea ng-change="field_modified()" 
+        class="form-control" ng-model="[% model %]"/>
     [% ELSIF field == 'post_code' %]
-      <input type="text" ng-blur="post_code_changed(patron.[% path %])"
+      <input type="text" ng-change="field_modified()" 
+        ng-blur="post_code_changed(patron.[% path %])"
         class="form-control" ng-model="[% model %]"/>
     [% ELSIF field == 'barcode' %]
       <input type="text" 
         focus-me="focus_bc"
+        ng-change="field_modified()" 
         ng-disabled="disable_bc"
         ng-blur="barcode_changed(patron.card.barcode)"
         class="form-control" ng-model="[% model %]"/>
     [% ELSIF field == 'usrname' %]
       <input type="text" 
         focus-me="focus_usrname"
+        ng-change="field_modified()" 
         ng-blur="usrname_changed(patron.usrname)"
         class="form-control" ng-model="[% model %]"/>
     [% ELSIF field == 'day_phone' %]
       <input type="text" 
         ng-blur="day_phone_changed(patron.day_phone)"
+        ng-change="field_modified()" 
         class="form-control" ng-model="[% model %]"/>
     [% ELSIF field.match('phone') %]
       <input type="text" 
+        ng-change="field_modified()" 
         ng-blur="dupe_value_changed('phone', patron.[% field %])"
         class="form-control" ng-model="[% model %]"/>
     [% ELSIF field.match('ident_value') %]
       <input type="text" 
+        ng-change="field_modified()" 
         ng-blur="dupe_value_changed('ident', patron.[% field %])"
         class="form-control" ng-model="[% model %]"/>
     [% ELSIF field == 'first_given_name' OR field == 'family_name' %]
       <input type="text" 
+        ng-change="field_modified()" 
         ng-blur="dupe_value_changed('name', patron.[% field %])"
         class="form-control" ng-model="[% model %]"/>
     [% ELSIF field == 'email' %]
       <input type="[% input_type %]" 
+        ng-change="field_modified()" 
         ng-blur="dupe_value_changed('email', patron.email)"
         class="form-control" ng-model="[% model %]"/>
     [% ELSIF field.match('street') OR field == 'city' %]
       <!-- note: passing address object to dupe_value_changed -->
       <input type="[% input_type %]" 
+        ng-change="field_modified()" 
         ng-blur="dupe_value_changed('address', patron.[% path %])"
         class="form-control" ng-model="[% model %]"/>
     [% ELSE %]
       <input type="[% input_type %]" 
+        ng-change="field_modified()" 
         class="form-control" ng-model="[% model %]"/>
     [% END %]
   [% END %]
@@ -230,6 +243,7 @@
   </div>
   <div class="col-md-3 reg-field-input">
     <input eg-date-input 
+      ng-change="field_modified()" 
       class="form-control" ng-model="patron.dob"/>
   </div>
 </div>
@@ -255,7 +269,9 @@
       </button>
       <ul class="dropdown-menu">
         <li ng-repeat="type in ident_types">
-          <a href ng-click="patron.ident_type = type">{{type.name()}}</a>
+          <a href ng-click="patron.ident_type = type; field_modified()">
+            {{type.name()}}
+          </a>
         </li>
       </ul>
     </div>
@@ -280,7 +296,7 @@
       src='[% DOC_IMG %]'></img>
     </div>
     <div class="col-md-3 reg-field-input">
-      <eg-org-selector selected="patron.home_ou" onchange="">
+      <eg-org-selector selected="patron.home_ou" onchange="field_modified">
       </eg-org-selector>
   </div>
 </div>
@@ -326,6 +342,7 @@
   </div>
   <div class="col-md-3 reg-field-input">
     <input eg-date-input 
+      ng-change="field_modified()" 
       class="form-control" ng-model="patron.expire_date"/>
   </div>
   <div class="col-md-3">
@@ -377,7 +394,9 @@
     <label>{{user_setting_types['opac.default_phone'].label()}}</label>
   </div>
   <div class="col-md-3 reg-field-input">
-    <input type='text' ng-model="user_settings['opac.default_phone']"/>
+    <input 
+      ng-change="field_modified()" 
+      type='text' ng-model="user_settings['opac.default_phone']"/>
   </div>
 </div>
 
@@ -386,7 +405,9 @@
     <label>{{user_setting_types['opac.default_pickup_location'].label()}}</label>
   </div>
   <div class="col-md-3 reg-field-input">
-    <eg-org-selector selected="patron.home_ou" onchange=""></eg-org-selector>
+    <eg-org-selector 
+      xonchange="field_modified" 
+      selected="patron.home_ou"></eg-org-selector>
   </div>
 </div>
 
@@ -396,7 +417,9 @@
   </div>
   <div class="col-md-3 reg-field-input">
     <div class='checkbox'>
-      <input type='checkbox' ng-model="user_settings['circ.holds_behind_desk']"/>
+      <input 
+        ng-change="field_modified()" 
+        type='checkbox' ng-model="user_settings['circ.holds_behind_desk']"/>
     </div>
   </div>
 </div>
@@ -407,15 +430,21 @@
   </div>
   <div class="col-md-3 reg-field-input flex-row">
     <div class='flex-cell'>
-      <input type='checkbox' ng-model="hold_notify_phone"/>
+      <input 
+        ng-change="field_modified()" 
+        type='checkbox' ng-model="hold_notify_phone"/>
       [% l('Phone') %]
     </div>
     <div class='flex-cell'>
-      <input type='checkbox' ng-model="hold_notify_email"/>
+      <input 
+        ng-change="field_modified()" 
+        type='checkbox' ng-model="hold_notify_email"/>
       [% l('Email') %]
     </div>
     <div class='flex-cell' ng-if="org_settings['sms.enable']">
-      <input type='checkbox' ng-model="hold_notify_sms"/>
+      <input 
+        ng-change="field_modified()" 
+        type='checkbox' ng-model="hold_notify_sms"/>
       [% l('SMS') %]
     </div>
   </div>
@@ -426,7 +455,9 @@
     <label>[% l('Default SMS/Text Number') %]</label>
   </div>
   <div class="col-md-3 reg-field-input">
-    <input type='text'/>
+    <input 
+      ng-change="field_modified()" 
+      type='text'/>
   </div>
 </div>
 
@@ -443,7 +474,7 @@
       <ul class="dropdown-menu">
         <li ng-repeat="carrier in sms_carriers">
           <a href 
-            ng-click="user_settings['opac.default_sms_carrier'] = carrier">
+            ng-click="field_modified();user_settings['opac.default_sms_carrier'] = carrier">
                 {{carrier.name()}}
           </a>
         </li>
@@ -457,7 +488,9 @@
     <label>{{type.label()}}</label>
   </div>
   <div class="col-md-3 reg-field-input">
-    <input type='checkbox' ng-model="user_settings[type.name()]"/>
+    <input 
+      ng-change="field_modified()" 
+      type='checkbox' ng-model="user_settings[type.name()]"/>
   </div>
 </div>
 
@@ -469,16 +502,17 @@
       <div class="col-md-3">
           <span class='pad-all-min'>
             [% l('Mailing') %] <input type='checkbox' 
-              ng-change="set_addr_type(addr, 'mailing')" 
+              ng-change="field_modified();set_addr_type(addr, 'mailing')" 
               ng-model="addr._is_mailing"/>
           </span>
           <span class='pad-all-min'>
             [% l('Physical') %] <input type='checkbox' 
-              ng-change="set_addr_type(addr, 'billing')" 
+              ng-change="field_modified();set_addr_type(addr, 'billing')" 
               ng-model="addr._is_billing"/>
           </span>
           <span class='pad-all-min'>
-            <button type="button" ng-click="delete_address(addr.id)" 
+            <button type="button" 
+              ng-click="field_modified();delete_address(addr.id)" 
               class="btn btn-danger">[% l('X') %]</button>
           </span>
       </div>
@@ -516,7 +550,9 @@
   </div>
   <div class="col-md-3 reg-field-input">
     <div ng-if="cat.entries().length == 0">
-      <input type="text" class="form-control"/>
+      <input 
+        ng-change="field_modified()" 
+        type="text" class="form-control"/>
     </div>
     <div ng-if="cat.entries().length != 0">
       <div class="btn-group" dropdown>
@@ -527,7 +563,8 @@
         </button>
         <ul class="dropdown-menu">
           <li ng-repeat="entry in cat.entries()">
-            <a href ng-click="stat_cat_entry_maps[cat.id()]=entry"> 
+            <a href 
+              ng-click="field_modified();stat_cat_entry_maps[cat.id()]=entry"> 
               {{entry.value()}}
             </a>
           </li>
@@ -563,7 +600,8 @@
           </button>
           <ul class="dropdown-menu">
             <li ng-repeat="answer in question.answers()">
-              <a href ng-click="survey_responses[question.id()] = answer"> 
+              <a href 
+                ng-click="field_modified();survey_responses[question.id()] = answer"> 
                 {{answer.answer()}} 
               </a>
             </li>
diff --git a/Open-ILS/src/templates/staff/css/circ.css.tt2 b/Open-ILS/src/templates/staff/css/circ.css.tt2
index f101d5c..e381543 100644
--- a/Open-ILS/src/templates/staff/css/circ.css.tt2
+++ b/Open-ILS/src/templates/staff/css/circ.css.tt2
@@ -102,8 +102,10 @@ but the ones I'm finding aren't quite cutting it..*/
     position: fixed;
     top:160px;
     right:20px;
-    /*border:2px dashed #d9e8f9;*/
+    /*
+    border:2px dashed #d9e8f9;
     -moz-border-radius: 10px;
+    */
     font-weight: bold;
     padding: 20px;
     margin-top: 20px;
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 f03a4eb..d9dc78f 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
@@ -706,8 +706,12 @@ function PatronRegCtrl($scope, $routeParams,
     $scope.edit_passthru.vis_level = 0; 
     // TODO: add save/clone handlers here
 
-    // TODO: call attach() on the first instance of a modified value
-    //egUnloadPrompt.attach($scope);
+    var modify_tracked = false;
+    $scope.field_modified = function() {
+        if (modify_tracked) return;
+        modify_tracked = true;
+        egUnloadPrompt.attach($scope);
+    }
 
     // Apply default values for new patrons during initial registration
     // prs is shorthand for patronSvc
@@ -881,6 +885,7 @@ function PatronRegCtrl($scope, $routeParams,
     $scope.set_profile = function(grp) {
         $scope.patron.profile = grp;
         $scope.set_expire_date();
+        $scope.field_modified();
     }
 
     $scope.new_address = function() {
@@ -1118,6 +1123,9 @@ function PatronRegCtrl($scope, $routeParams,
 
     $scope.edit_passthru.save = function() {
 
+        // remove page unload warning prompt
+        egUnloadPrompt.clear();
+
         // toss the deleted addresses back into the patron's list of
         // addresses so it's included in the update
         $scope.patron.addresses = 
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 e2c6b93..2ffbc22 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -186,7 +186,8 @@ function($modal, $interpolate) {
 
 /**
  * Warn on page unload and give the user a chance to avoid navigating
- * away from the current page.
+ * away from the current page.  
+ * Only one handler is supported per page.
  * NOTE: we can't use an egUnloadDialog as the dialog builder, because
  * it renders asynchronously, which allows the page to redirect before
  * the dialog appears.
@@ -196,6 +197,7 @@ function($modal, $interpolate) {
 function($window , egStrings) {
     var service = {};
 
+    // attach a page/scope unload prompt
     service.attach = function($scope, msg) {
 
         // handle page change
@@ -203,12 +205,24 @@ function($window , egStrings) {
             return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
         });
 
-        // handle controller change (e.g. tabbed navigation)
-        $scope.$on('$locationChangeStart', function(evt, next, current) {
+        if (!$scope) return;
+
+        // If a scope was provided, attach a scope-change handler,
+        // similar to the page-page prompt.
+        service.locChangeCancel = 
+            $scope.$on('$locationChangeStart', function(evt, next, current) {
             if (!confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) 
                 evt.preventDefault();
         });
     };
+
+    // remove the page unload prompt
+    service.clear = function() {
+        $($window).off('beforeunload');
+        if (service.locChangeCancel)
+            service.locChangeCancel();
+    }
+
     return service;
 }])
 

commit 12477de2c98c3ea45b35577a6892003f1341fb9d
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sat Jan 16 11:42:59 2016 -0500

    LP#1452950 Secondary groups dialog scrollable groups list
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_patron_groups_dialog.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_patron_groups_dialog.tt2
index 8fd18cf..5dd8f3e 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_patron_groups_dialog.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_patron_groups_dialog.tt2
@@ -34,7 +34,7 @@ This does not affect circulation policy.
                 <span style="padding-right: 5px;">{{args.new_profile.name()}}</span>
                 <span class="caret"></span>
               </button>
-              <ul class="dropdown-menu">
+              <ul class="dropdown-menu scrollable-menu">
                 <li ng-repeat="grp in args.edit_profiles">
                   <a href
                     style="padding-left: {{pgt_depth(grp) * 10 + 5}}px"

commit 58d0590e07d3f4db68c9a6ae8b90a6f4f48aa4c4
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sat Jan 16 11:39:08 2016 -0500

    LP#1452950 Patron reg pass=phone on new patrons only
    
    When changing a patron's phone number, only modify the password for new
    patrons when the org setting patron.password.use_phone is enabled.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 6670b76..f03a4eb 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
@@ -939,8 +939,9 @@ function PatronRegCtrl($scope, $routeParams,
     }
 
     $scope.day_phone_changed = function(phone) {
-        if (phone && $scope.org_settings['patron.password.use_phone']) {
-           $scope.patron.passwd = phone.substr(-4);
+        if (phone && $scope.patron.isnew && 
+            $scope.org_settings['patron.password.use_phone']) {
+            $scope.patron.passwd = phone.substr(-4);
         }
     }
 

commit 6343de1a223951b00d613326fc95aa8eb5a274c4
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sat Jan 16 11:34:44 2016 -0500

    LP#1452950 Patron reg avoid data re-fetch
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 2e5d932..6670b76 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
@@ -12,15 +12,27 @@ angular.module('egCoreMod')
         user_settings : {},        // applied user settings
         user_setting_types : {},   // config.usr_setting_type
         opt_in_setting_types : {}, // config.usr_setting_type for event-def opt-in
+        surveys : [],
         survey_questions : {},
         survey_answers : {},
         survey_responses : {},     // survey.responses for loaded patron in progress
+        stat_cats : [],
         stat_cat_entry_maps : {},   // cat.id to selected entry object map
-        virt_id : -1               // virtual ID for new objects
+        virt_id : -1,               // virtual ID for new objects
+        init_done : false           // have we loaded our initialization data?
     };
 
     // launch a series of parallel data retrieval calls
     service.init = function(scope) {
+
+        // Data loaded here only needs to be retrieved the first time this
+        // tab becomes active within the current instance of the patron app.
+        // In other words, navigating between patron tabs will not cause
+        // all of this data to be reloaded.  Navigating to a separate app
+        // and returning will cause the data to be reloaded.
+        if (service.init_done) return $q.when();
+        service.init_done = true;
+
         return $q.all([
             service.get_field_doc(),
             service.get_perm_groups(),
@@ -281,7 +293,6 @@ angular.module('egCoreMod')
     }
 
     service.get_field_doc = function() {
-
         return egCore.pcrud.search('fdoc', {
             fm_class: ['au', 'ac', 'aua', 'actsc', 'asv', 'asvq', 'asva']})
         .then(null, null, function(doc) {

commit 34353e9714c9a619449ff5d287041e7f4f550f30
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sat Jan 16 11:33:38 2016 -0500

    LP#1452950 Remove dupe/doc floating div dashed border
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/css/circ.css.tt2 b/Open-ILS/src/templates/staff/css/circ.css.tt2
index 599b2d5..f101d5c 100644
--- a/Open-ILS/src/templates/staff/css/circ.css.tt2
+++ b/Open-ILS/src/templates/staff/css/circ.css.tt2
@@ -102,7 +102,7 @@ but the ones I'm finding aren't quite cutting it..*/
     position: fixed;
     top:160px;
     right:20px;
-    border:2px dashed #d9e8f9;
+    /*border:2px dashed #d9e8f9;*/
     -moz-border-radius: 10px;
     font-weight: bold;
     padding: 20px;

commit a05d0181b384f5716a88b4d2b19c693d1f3fc0ec
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Dec 16 21:11:44 2015 -0500

    LP#1452950 patron reg. unload prompt stub
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 ed016fc..2e5d932 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
@@ -671,7 +671,7 @@ angular.module('egCoreMod')
 
 
 function PatronRegCtrl($scope, $routeParams, 
-    $q, $modal, $window, egCore, patronSvc, patronRegSvc) {
+    $q, $modal, $window, egCore, patronSvc, patronRegSvc, egUnloadPrompt) {
 
     $scope.page_data_loaded = false;
     $scope.clone_id = $routeParams.clone_id;
@@ -695,6 +695,9 @@ function PatronRegCtrl($scope, $routeParams,
     $scope.edit_passthru.vis_level = 0; 
     // TODO: add save/clone handlers here
 
+    // TODO: call attach() on the first instance of a modified value
+    //egUnloadPrompt.attach($scope);
+
     // Apply default values for new patrons during initial registration
     // prs is shorthand for patronSvc
     function set_new_patron_defaults(prs) {
@@ -1131,5 +1134,5 @@ function PatronRegCtrl($scope, $routeParams,
 // This controller may be loaded from different modules (patron edit vs.
 // register new patron), so we have to inject the controller params manually.
 PatronRegCtrl.$inject = ['$scope', '$routeParams', '$q', '$modal', 
-    '$window', 'egCore', 'patronSvc', 'patronRegSvc'];
+    '$window', 'egCore', 'patronSvc', 'patronRegSvc', 'egUnloadPrompt'];
 

commit cdab4c76d7f8b077aa838b1b9208fb4e920c7aeb
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Dec 16 21:10:43 2015 -0500

    LP#1452950 page unload warning prompt service
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    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 db4f85a..c2b1abb 100644
--- a/Open-ILS/src/templates/staff/base_js.tt2
+++ b/Open-ILS/src/templates/staff/base_js.tt2
@@ -48,5 +48,13 @@
   // pending api_level thunking in C
   // OpenSRF.api_level = 2;
   OpenSRF.Session.transport = OSRF_TRANSPORT_TYPE_WS;
+
+  // Here lie JS strings that may be used on any/all pages.
+  angular.module('egCoreMod').run(['egStrings', function(s) {
+    s.EG_UNLOAD_CTRL_PROMPT_MSG = 
+      '[% l('This page may have unsaved data.\n\nAre you sure you want to leave this page?') %]';
+    s.EG_UNLOAD_PAGE_PROMPT_MSG = 
+      '[% l('This page may have unsaved data.') %]';
+  }]);
 </script>
 
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 4fe04a7..e2c6b93 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -184,6 +184,34 @@ function($modal, $interpolate) {
     return service;
 }])
 
+/**
+ * Warn on page unload and give the user a chance to avoid navigating
+ * away from the current page.
+ * NOTE: we can't use an egUnloadDialog as the dialog builder, because
+ * it renders asynchronously, which allows the page to redirect before
+ * the dialog appears.
+ */
+.factory('egUnloadPrompt', [
+        '$window','egStrings', 
+function($window , egStrings) {
+    var service = {};
+
+    service.attach = function($scope, msg) {
+
+        // handle page change
+        $($window).on('beforeunload', function() { 
+            return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
+        });
+
+        // handle controller change (e.g. tabbed navigation)
+        $scope.$on('$locationChangeStart', function(evt, next, current) {
+            if (!confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) 
+                evt.preventDefault();
+        });
+    };
+    return service;
+}])
+
 .directive('aDisabled', function() {
     return {
         restrict : 'A',

commit 94ed2db118a8e66ef2d82f305c3017075574a45c
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Dec 10 10:09:50 2015 -0500

    LP#1452950 patron reg dupe links wording consistency
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index dba0a4c..0879eb4 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -51,7 +51,7 @@
 		<div>
 			<a target="_blank" ng-show="dupe_counts.address"
 				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
-				[% l('Found [_1] patron(s) with same address', 
+				[% l('[_1] patron(s) with same address', 
 				'{{dupe_counts.address}}') %]</a>
 		</div>
 	</div>

commit 403a9371a2bb5f73d5c48c71bd4327842ecc250b
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Dec 10 10:06:45 2015 -0500

    LP#1452950 patron reg refactor alert pane
    
    Create a single top-right floating alert pane which contains both the
    duplicate patron links and the field documentation.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 23e51d7..dba0a4c 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -19,46 +19,50 @@
 <div ng-if="patron_id"
     class="strong-text-2">[% l('Patron Edit') %]</div>
 
-<!-- IDL field documentation window -->
-<fieldset id="reg-field-doc" ng-show="selected_field_doc">
-  <legend>
-  {{idl_fields[selected_field_doc.fm_class()][selected_field_doc.field()].label}}
-  </legend>
-  <div>{{selected_field_doc.string()}}</div>
-</fieldset>
-
-<div id="reg-dupe-links">
-    [%# dupe_search_encoded is uri escaped in the JS %]
-    <div>
-        <a target="_blank" ng-show="dupe_counts.name"
-            href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
-        [% l('Found [_1] patron(s) with the same name', '{{dupe_counts.name}}') %]
-        </a>
-    </div>
-    <div>
-        <a target="_blank" ng-show="dupe_counts.email"
-            href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
-            [% l('Found [_1] patron(s) with the same email address', 
-            '{{dupe_counts.email}}') %]</a>
-    </div>
-    <div>
-        <a target="_blank" ng-show="dupe_counts.ident"
-            href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
-            [% l('Found [_1] patron(s) with the same identification', 
-            '{{dupe_counts.ident}}') %]</a>
-    </div>
-    <div>
-        <a target="_blank" ng-show="dupe_counts.phone"
-            href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
-            [% l('Found [_1] patron(s) with the same phone number', 
-            '{{dupe_counts.phone}}') %]</a>
-    </div>
-    <div>
-        <a target="_blank" ng-show="dupe_counts.address"
-            href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
-            [% l('Found [_1] patron(s) with the same address', 
-            '{{dupe_counts.address}}') %]</a>
-    </div>
+
+<div id="reg-alert-pane">
+
+	<div id="reg-dupe-links">
+		[%# dupe_search_encoded is uri escaped in the JS %]
+		<div>
+			<a target="_blank" ng-show="dupe_counts.name"
+				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+			[% l('[_1] patron(s) with same name', '{{dupe_counts.name}}') %]
+			</a>
+		</div>
+		<div>
+			<a target="_blank" ng-show="dupe_counts.email"
+				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+				[% l('[_1] patron(s) with same email', 
+				'{{dupe_counts.email}}') %]</a>
+		</div>
+		<div>
+			<a target="_blank" ng-show="dupe_counts.ident"
+				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+				[% l('[_1] patron(s) with same identification', 
+				'{{dupe_counts.ident}}') %]</a>
+		</div>
+		<div>
+			<a target="_blank" ng-show="dupe_counts.phone"
+				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+				[% l('[_1] patron(s) with same phone', 
+				'{{dupe_counts.phone}}') %]</a>
+		</div>
+		<div>
+			<a target="_blank" ng-show="dupe_counts.address"
+				href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+				[% l('Found [_1] patron(s) with same address', 
+				'{{dupe_counts.address}}') %]</a>
+		</div>
+	</div>
+
+	<!-- IDL field documentation window -->
+	<fieldset id="reg-field-doc" ng-show="selected_field_doc">
+		<legend>
+		{{idl_fields[selected_field_doc.fm_class()][selected_field_doc.field()].label}}
+		</legend>
+		<div>{{selected_field_doc.string()}}</div>
+	</fieldset>
 </div>
 
 
diff --git a/Open-ILS/src/templates/staff/css/circ.css.tt2 b/Open-ILS/src/templates/staff/css/circ.css.tt2
index 80373bb..599b2d5 100644
--- a/Open-ILS/src/templates/staff/css/circ.css.tt2
+++ b/Open-ILS/src/templates/staff/css/circ.css.tt2
@@ -97,9 +97,8 @@ but the ones I'm finding aren't quite cutting it..*/
   text-align: right;
 }
 
-
 /* floating div along top-right with field documentation */
-#reg-field-doc {
+#reg-alert-pane {
     position: fixed;
     top:160px;
     right:20px;
@@ -111,13 +110,10 @@ but the ones I'm finding aren't quite cutting it..*/
 }
 
 #reg-dupe-links {
-    position: fixed;
-    top:180px;
-    right:20px;
-    padding: 10px;
-    margin-top: 10px;
+		margin-bottom: 10px;
 }
 
+
 #reg-field-doc legend {
     /* otherwise the font size is quite large */
     font-size: 100%;

commit d69bb376a59fd8dc2db95a26bccbc599c54defc7
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Dec 9 11:42:50 2015 -0500

    LP#1452950 patron dupe search links
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 419c23c..23e51d7 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -27,6 +27,41 @@
   <div>{{selected_field_doc.string()}}</div>
 </fieldset>
 
+<div id="reg-dupe-links">
+    [%# dupe_search_encoded is uri escaped in the JS %]
+    <div>
+        <a target="_blank" ng-show="dupe_counts.name"
+            href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+        [% l('Found [_1] patron(s) with the same name', '{{dupe_counts.name}}') %]
+        </a>
+    </div>
+    <div>
+        <a target="_blank" ng-show="dupe_counts.email"
+            href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+            [% l('Found [_1] patron(s) with the same email address', 
+            '{{dupe_counts.email}}') %]</a>
+    </div>
+    <div>
+        <a target="_blank" ng-show="dupe_counts.ident"
+            href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+            [% l('Found [_1] patron(s) with the same identification', 
+            '{{dupe_counts.ident}}') %]</a>
+    </div>
+    <div>
+        <a target="_blank" ng-show="dupe_counts.phone"
+            href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+            [% l('Found [_1] patron(s) with the same phone number', 
+            '{{dupe_counts.phone}}') %]</a>
+    </div>
+    <div>
+        <a target="_blank" ng-show="dupe_counts.address"
+            href="/eg/staff/circ/patron/search?search={{dupe_search_encoded}}" >
+            [% l('Found [_1] patron(s) with the same address', 
+            '{{dupe_counts.address}}') %]</a>
+    </div>
+</div>
+
+
 [% MACRO formfield(cls, field, path, input_type) BLOCK;
 
   # input field generator for common text/number/checkbox fields
diff --git a/Open-ILS/src/templates/staff/css/circ.css.tt2 b/Open-ILS/src/templates/staff/css/circ.css.tt2
index efc04f7..80373bb 100644
--- a/Open-ILS/src/templates/staff/css/circ.css.tt2
+++ b/Open-ILS/src/templates/staff/css/circ.css.tt2
@@ -110,6 +110,14 @@ but the ones I'm finding aren't quite cutting it..*/
     margin-top: 20px;
 }
 
+#reg-dupe-links {
+    position: fixed;
+    top:180px;
+    right:20px;
+    padding: 10px;
+    margin-top: 10px;
+}
+
 #reg-field-doc legend {
     /* otherwise the font size is quite large */
     font-size: 100%;
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 2cd7d9d..6a184ae 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
@@ -793,11 +793,16 @@ function($scope,  $q,  $routeParams,  $timeout,  $window,  $location,  egCore,
 
     // Handle URL-encoded searches
     if ($location.search().search) {
+        console.log('URL search = ' + $location.search().search);
         patronSvc.urlSearch = {search : JSON2js($location.search().search)};
 
         // why the double-JSON encoded sort?
-        patronSvc.urlSearch.sort = 
-            JSON2js(patronSvc.urlSearch.search.search_sort);
+        if (patronSvc.urlSearch.search.search_sort) {
+            patronSvc.urlSearch.sort = 
+                JSON2js(patronSvc.urlSearch.search.search_sort);
+        } else {
+            patronSvc.urlSearch.sort = [];
+        }
         delete patronSvc.urlSearch.search.search_sort;
     }
 
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 739e098..ed016fc 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
@@ -381,9 +381,6 @@ angular.module('egCoreMod')
 
         switch (type) {
 
-            // TODO hide dupe results links matching the type 
-            // of the current search
-
             case 'name':
                 var fname = patron.first_given_name;   
                 var lname = patron.family_name;   
@@ -422,11 +419,11 @@ angular.module('egCoreMod')
             'open-ils.actor.patron.search.advanced',
             egCore.auth.token(), search, null, null, 1
         ).then(function(res) {
-
             res = res.filter(function(id) {return id != patron.id});
-            if (res.length == 0) return;
-
-            console.log(js2JSON(res));
+            return {
+                count : res.length,
+                search : search
+            };
         });
     }
 
@@ -685,6 +682,7 @@ function PatronRegCtrl($scope, $routeParams,
     // for existing patrons, disable barcode input by default
     $scope.disable_bc = $scope.focus_usrname = Boolean($scope.patron_id);
     $scope.focus_bc = !Boolean($scope.patron_id);
+    $scope.dupe_counts = {};
 
     if (!$scope.edit_passthru) {
         // in edit more, scope.edit_passthru is delivered to us by
@@ -1094,7 +1092,13 @@ function PatronRegCtrl($scope, $routeParams,
     }
 
     $scope.dupe_value_changed = function(type, value) {
-        patronRegSvc.dupe_patron_search($scope.patron, type, value);
+        $scope.dupe_counts[type] = 0;
+        patronRegSvc.dupe_patron_search($scope.patron, type, value)
+        .then(function(res) {
+            $scope.dupe_counts[type] = res.count;
+            $scope.dupe_search_encoded = 
+                encodeURIComponent(js2JSON(res.search));
+        });
     }
 
     $scope.edit_passthru.save = function() {

commit 0c355ea9d8f5420efa138cd7b279fb1874ea265a
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Dec 8 16:17:40 2015 -0500

    LP#1452950 patron dupe search / plumbing
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 980ec0c..419c23c 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -79,6 +79,27 @@
       <input type="text" 
         ng-blur="day_phone_changed(patron.day_phone)"
         class="form-control" ng-model="[% model %]"/>
+    [% ELSIF field.match('phone') %]
+      <input type="text" 
+        ng-blur="dupe_value_changed('phone', patron.[% field %])"
+        class="form-control" ng-model="[% model %]"/>
+    [% ELSIF field.match('ident_value') %]
+      <input type="text" 
+        ng-blur="dupe_value_changed('ident', patron.[% field %])"
+        class="form-control" ng-model="[% model %]"/>
+    [% ELSIF field == 'first_given_name' OR field == 'family_name' %]
+      <input type="text" 
+        ng-blur="dupe_value_changed('name', patron.[% field %])"
+        class="form-control" ng-model="[% model %]"/>
+    [% ELSIF field == 'email' %]
+      <input type="[% input_type %]" 
+        ng-blur="dupe_value_changed('email', patron.email)"
+        class="form-control" ng-model="[% model %]"/>
+    [% ELSIF field.match('street') OR field == 'city' %]
+      <!-- note: passing address object to dupe_value_changed -->
+      <input type="[% input_type %]" 
+        ng-blur="dupe_value_changed('address', patron.[% path %])"
+        class="form-control" ng-model="[% model %]"/>
     [% ELSE %]
       <input type="[% input_type %]" 
         class="form-control" ng-model="[% model %]"/>
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 44f936c..739e098 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
@@ -373,6 +373,63 @@ angular.module('egCoreMod')
         });
     }
 
+    service.dupe_patron_search = function(patron, type, value) {
+        var search;
+
+        console.log('Dupe search called with "' + 
+            type +"' and value " + value);
+
+        switch (type) {
+
+            // TODO hide dupe results links matching the type 
+            // of the current search
+
+            case 'name':
+                var fname = patron.first_given_name;   
+                var lname = patron.family_name;   
+                if (!(fname && lname)) return;
+                search = {
+                    first_given_name : {value : fname, group : 0},
+                    family_name : {value : lname, group : 0}
+                };
+                break;
+
+            case 'email':
+                search = {email : {value : value, group : 0}};
+                break;
+
+            case 'ident':
+                search = {ident : {value : value, group : 2}};
+                break;
+
+            case 'phone':
+                search = {phone : {value : value, group : 2}};
+                break;
+
+            case 'address':
+                search = {};
+                angular.forEach(['street1', 'street2', 'city', 'post_code'],
+                    function(field) {
+                        if(value[field])
+                            search[field] = {value : value[field], group: 1};
+                    }
+                );
+                break;
+        }
+
+        return egCore.net.request( 
+            'open-ils.actor', 
+            'open-ils.actor.patron.search.advanced',
+            egCore.auth.token(), search, null, null, 1
+        ).then(function(res) {
+
+            res = res.filter(function(id) {return id != patron.id});
+            if (res.length == 0) return;
+
+            console.log(js2JSON(res));
+        });
+    }
+
     service.init_patron = function(current) {
 
         if (!current)
@@ -481,8 +538,7 @@ angular.module('egCoreMod')
         var patron = egCore.idl.fromHash('au', phash);
 
         patron.home_ou(patron.home_ou().id());
-        patron.expire_date(
-            patron.expire_date().toISOString().replace(/T.*/,''));
+        patron.expire_date(patron.expire_date().toISOString());
         patron.profile(patron.profile().id());
         if (patron.dob()) 
             patron.dob(patron.dob().toISOString().replace(/T.*/,''));
@@ -1037,6 +1093,10 @@ function PatronRegCtrl($scope, $routeParams,
         patronRegSvc.invalidate_field($scope.patron, field);
     }
 
+    $scope.dupe_value_changed = function(type, value) {
+        patronRegSvc.dupe_patron_search($scope.patron, type, value);
+    }
+
     $scope.edit_passthru.save = function() {
 
         // toss the deleted addresses back into the patron's list of

commit a5d5d19bd7dac767fc37f272858c286f6acf37a8
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Dec 8 09:39:43 2015 -0500

    LP#1452950 patron reg invalidate fields
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index c8bfb8c..980ec0c 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -107,8 +107,11 @@
       <!-- invalidate buttons -->
 
       [% IF field.match('phone') OR field.match('email') %]
-        <button ng-show="patron.[% field %] && !patron.isnew" class="btn btn-default"
-          ng-click="">[% l('Invalidate') %]</button>
+        <button ng-show="patron.[% field %] && !patron.isnew" 
+            class="btn btn-default" 
+            ng-click="invalidate_field('[% field %]')">
+            [% l('Invalidate') %]
+        </button>
       [% END %]
 
       <!-- example strings -->
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 8514b09..44f936c 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
@@ -355,6 +355,24 @@ angular.module('egCoreMod')
         });
     }
 
+    service.invalidate_field = function(patron, field) {
+        console.log('Invalidating patron field ' + field);
+
+        return egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.invalidate.' + field,
+            egCore.auth.token(), patron.id, null, patron.home_ou.id()
+
+        ).then(function(res) {
+            // clear the invalid value from the form
+            patron[field] = '';
+
+            // update last_xact_id so future save operations
+            // on this patron will be allowed
+            patron.last_xact_id = res.payload.last_xact_id[patron.id];
+        });
+    }
+
     service.init_patron = function(current) {
 
         if (!current)
@@ -387,7 +405,8 @@ angular.module('egCoreMod')
 
         patron.home_ou = egCore.org.get(patron.home_ou.id);
         patron.expire_date = new Date(Date.parse(patron.expire_date));
-        patron.dob = new Date(Date.parse(patron.dob));
+        patron.dob = patron.dob ?
+            new Date(Date.parse(patron.dob)) : null;
         patron.profile = current.profile(); // pre-hash version
         patron.net_access_level = current.net_access_level();
         patron.ident_type = current.ident_type();
@@ -1014,6 +1033,10 @@ function PatronRegCtrl($scope, $routeParams,
         $scope.hold_notify_sms = Boolean(notify.match(/sms/));
     }
 
+    $scope.invalidate_field = function(field) {
+        patronRegSvc.invalidate_field($scope.patron, field);
+    }
+
     $scope.edit_passthru.save = function() {
 
         // toss the deleted addresses back into the patron's list of

commit ade2612ee97f51588aaa848035e7a1ad700d9540
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Nov 22 20:34:28 2015 -0500

    LP#1452950 patron reg secondary groups done
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 6e08117..c8bfb8c 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -240,7 +240,7 @@
         <span class="caret"></span>
       </button>
       <ul class="dropdown-menu">
-        <li ng-repeat="grp in profiles">
+        <li ng-repeat="grp in edit_profiles">
           <a href 
             style="padding-left: {{pgt_depth(grp) * 10 + 5}}px"
             ng-click="set_profile(grp)">{{grp.name()}}</a>
@@ -249,9 +249,8 @@
     </div>
   </div>
   <div class="col-md-3">
-    <button class="btn btn-default" ng-click="secondary_groups_dialog()">
-        [% l('Secondary Groups') %]
-    </button>
+    <button class="btn btn-default" ng-disabled="!has_group_link_perm"
+      ng-click="secondary_groups_dialog()">[% l('Secondary Groups') %]</button>
   </div> 
 </div>
 
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_patron_groups_dialog.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_patron_groups_dialog.tt2
index 7cdd889..8fd18cf 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_patron_groups_dialog.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_patron_groups_dialog.tt2
@@ -29,10 +29,24 @@ This does not affect circulation policy.
       </div>
       <div class="row pad-all-min">
         <div class="col-md-6">
-            <!-- group selector -->
+            <div class="btn-group" dropdown>
+              <button type="button" class="btn btn-default dropdown-toggle">
+                <span style="padding-right: 5px;">{{args.new_profile.name()}}</span>
+                <span class="caret"></span>
+              </button>
+              <ul class="dropdown-menu">
+                <li ng-repeat="grp in args.edit_profiles">
+                  <a href
+                    style="padding-left: {{pgt_depth(grp) * 10 + 5}}px"
+                    ng-click="args.new_profile = grp">{{grp.name()}}</a>
+                </li>
+              </ul>
+            </div>
+
         </div>
         <div class="col-md-6">
-            <button class="btn btn-default" ng-click="link_group($event, grp)">
+            <button class="btn btn-default" 
+              ng-click="link_group($event, args.new_profile)">
                 [% l('Add') %]
             </button>
         </div>
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 54eb55c..8514b09 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
@@ -7,7 +7,7 @@ angular.module('egCoreMod')
     var service = {
         field_doc : {},            // config.idl_field_doc
         profiles : [],             // permission groups
-        no_edit_profiles : [],     // perm groups we cannot modify
+        edit_profiles : [],        // perm groups we can modify
         sms_carriers : [],
         user_settings : {},        // applied user settings
         user_setting_types : {},   // config.usr_setting_type
@@ -36,7 +36,7 @@ angular.module('egCoreMod')
     //service.check_grp_app_perm = function(grp_id) {
 
     // determine which user groups our user is not allowed to modify
-    service.set_no_edit_profiles = function() {
+    service.set_edit_profiles = function() {
         var all_app_perms = [];
         var failed_perms = [];
 
@@ -46,12 +46,12 @@ angular.module('egCoreMod')
                 all_app_perms.push(grp.application_perm());
         }); 
 
-        // fill in service.no_edit_profiles by inspecting failed_perms
+        // fill in service.edit_profiles by inspecting failed_perms
         function traverse_grp_tree(grp, failed) {
             failed = failed || 
                 failed_perms.indexOf(grp.application_perm()) > -1;
 
-            if (failed) service.no_edit_profiles.push(grp.id());
+            if (!failed) service.edit_profiles.push(grp);
 
             angular.forEach(
                 service.profiles.filter( // children of grp
@@ -62,20 +62,19 @@ angular.module('egCoreMod')
 
         return egCore.perm.hasPermAt(all_app_perms, true).then(
             function(perm_orgs) {
-
                 angular.forEach(all_app_perms, function(p) {
                     if (perm_orgs[p].length == 0)
                         failed_perms.push(p);
                 });
 
                 traverse_grp_tree(egCore.env.pgt.tree);
-                console.log('User is not allowed to edit profiles: ' 
-                    + service.no_edit_profiles);
             }
         );
     }
 
-    service.group_link_perms = function() {
+    service.has_group_link_perms = function(org_id) {
+        return egCore.perm.hasPermAt('CREATE_USER_GROUP_LINK', true)
+        .then(function(p) { return p.indexOf(org_id) > -1; });
     }
 
     service.get_surveys = function() {
@@ -267,7 +266,7 @@ angular.module('egCoreMod')
     service.get_perm_groups = function() {
         if (egCore.env.pgt) {
             service.profiles = egCore.env.pgt.list;
-            return service.set_no_edit_profiles();
+            return service.set_edit_profiles();
         } else {
             return egCore.pcrud.search('pgt', {parent : null}, 
                 {flesh : -1, flesh_fields : {pgt : ['children']}}
@@ -275,7 +274,7 @@ angular.module('egCoreMod')
                 function(tree) {
                     egCore.env.absorbTree(tree, 'pgt')
                     service.profiles = egCore.env.pgt.list;
-                    return service.set_no_edit_profiles();
+                    return service.set_edit_profiles();
                 }
             );
         }
@@ -650,6 +649,13 @@ function PatronRegCtrl($scope, $routeParams,
         }
     }
 
+    function handle_home_org_changed() {
+        org_id = $scope.patron.home_ou.id();
+
+        patronRegSvc.has_group_link_perms(org_id)
+        .then(function(bool) {$scope.has_group_link_perm = bool});
+    }
+
     $q.all([
 
         $scope.initTab ? // initTab comes from patron app
@@ -664,7 +670,7 @@ function PatronRegCtrl($scope, $routeParams,
         // in standalone mode, we have no patronSvc
         $scope.patron = prs.init_patron(patronSvc ? patronSvc.current : null);
         $scope.field_doc = prs.field_doc;
-        $scope.profiles = prs.profiles;
+        $scope.edit_profiles = prs.edit_profiles;
         $scope.ident_types = prs.ident_types;
         $scope.net_access_levels = prs.net_access_levels;
         $scope.user_setting_types = prs.user_setting_types;
@@ -685,6 +691,7 @@ function PatronRegCtrl($scope, $routeParams,
         });
 
         extract_hold_notify();
+        handle_home_org_changed();
 
         if ($scope.org_settings['ui.patron.edit.default_suggested'])
             $scope.edit_passthru.vis_level = 1;
@@ -939,17 +946,31 @@ function PatronRegCtrl($scope, $routeParams,
         $scope.user_settings['opac.hold_notify'] = hold_notify;
     }
 
+    // dialog for selecting additional permission groups
     $scope.secondary_groups_dialog = function() {
         $modal.open({
             templateUrl: './circ/patron/t_patron_groups_dialog',
             controller: 
-                   ['$scope','$modalInstance','linked_groups',
-            function($scope , $modalInstance , linked_groups) {
-                // scope here is the modal-level scope
-                $scope.link_group = function(grp) {
-                    $scope.args.linked_groups.push(grp);
+                   ['$scope','$modalInstance','linked_groups','pgt_depth',
+            function($scope , $modalInstance , linked_groups , pgt_depth) {
+
+                $scope.pgt_depth = pgt_depth;
+                $scope.args = {
+                    linked_groups : linked_groups,
+                    edit_profiles : patronRegSvc.edit_profiles,
+                    new_profile   : patronRegSvc.edit_profiles[0]
+                };
+
+                // add a new group to the linked groups list
+                $scope.link_group = function($event, grp) {
+                    var found = false; // avoid duplicates
+                    angular.forEach($scope.args.linked_groups, 
+                        function(g) {if (g.id() == grp.id()) found = true});
+                    if (!found) $scope.args.linked_groups.push(grp);
                     $event.preventDefault(); // avoid close
                 }
+
+                // remove a group from the linked groups list
                 $scope.unlink_group = function($event, grp) {
                     $scope.args.linked_groups = 
                         $scope.args.linked_groups.filter(function(g) {
@@ -957,20 +978,29 @@ function PatronRegCtrl($scope, $routeParams,
                     });
                     $event.preventDefault(); // avoid close
                 }
-                $scope.args = {linked_groups : linked_groups};
+
                 $scope.ok = function() { $modalInstance.close($scope.args) }
                 $scope.cancel = function () { $modalInstance.dismiss() }
             }],
             resolve : {
-                linked_groups : function() {
-                    // scope here is the controller-level scope
-                    return $scope.patron.groups;
-                }
+                linked_groups : function() { return $scope.patron.groups },
+                pgt_depth : function() { return $scope.pgt_depth }
             }
         }).result.then(
             function(args) {
-                angular.forEach(args.linked_groups, function(grp) {
-                    // TODO add/remove linked groups
+                var ids = args.linked_groups.map(function(g) {return g.id()});
+                console.log('linking permission groups ' + ids);
+                return egCore.net.request(
+                    'open-ils.actor',
+                    'open-ils.actor.user.set_groups',
+                    egCore.auth.token(), $scope.patron.id, ids)
+                .then(function(resp) {
+                    if (resp == 1) {
+                        $scope.patron.groups = args.linked_groups;
+                    } else {
+                        // debugging -- should be no events
+                        alert('linked groups failure ' + egCore.evt.parse(resp));
+                    }
                 });
             }
         );

commit eb1ceb99bc814437c3ce402a5fad279113308341
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Nov 19 10:04:10 2015 -0500

    LP#1452950 Patron reg secondary groups part 1
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index a35e9e4..6e08117 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -249,7 +249,9 @@
     </div>
   </div>
   <div class="col-md-3">
-    <button class="btn btn-default">[% l('Secondary Groups') %]</button>
+    <button class="btn btn-default" ng-click="secondary_groups_dialog()">
+        [% l('Secondary Groups') %]
+    </button>
   </div> 
 </div>
 
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_patron_groups_dialog.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_patron_groups_dialog.tt2
new file mode 100644
index 0000000..7cdd889
--- /dev/null
+++ b/Open-ILS/src/templates/staff/circ/patron/t_patron_groups_dialog.tt2
@@ -0,0 +1,46 @@
+<form ng-submit="ok(args)" role="form">
+    <div class="modal-header">
+      <button type="button" class="close" ng-click="cancel()" 
+        aria-hidden="true">×</button>
+      <h4 class="modal-title">[% l('Secondary Permission Groups') %]</h4>
+    </div>
+    <div class="modal-body patron-reg-barcodes">
+      <div class="row pad-all">
+      [% | l %]
+Assign additional permission groups to users here.  
+This does not affect circulation policy.
+      [% END %]
+      </div>
+      <div class="row header">
+        <div class="col-md-6">
+          <label>[% l('Group') %]</label>
+        </div>
+        <div class="col-md-6">
+          <label>[% l('Action') %]</label>
+        </div>
+      </div>
+      <div class="row pad-all-min" ng-repeat="grp in args.linked_groups">
+        <div class="col-md-6">{{grp.name()}}</div>
+        <div class="col-md-6">
+            <button class="btn btn-default" ng-click="unlink_group($event, grp)">
+                [% l('Delete') %]
+            </button>
+        </div>
+      </div>
+      <div class="row pad-all-min">
+        <div class="col-md-6">
+            <!-- group selector -->
+        </div>
+        <div class="col-md-6">
+            <button class="btn btn-default" ng-click="link_group($event, grp)">
+                [% l('Add') %]
+            </button>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <input type="submit" class="btn btn-primary" value="[% l('Apply Changes') %]"/>
+      <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+    </div>
+  </div> <!-- modal-content -->
+</form>
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 117d8f8..2cd7d9d 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
@@ -47,7 +47,8 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap',
                 'net_access_level',
                 'ident_type',
                 'ident_type2',
-                'cards'
+                'cards',
+                'groups'
             ]);
         }
 
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 dca92a8..54eb55c 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
@@ -7,6 +7,7 @@ angular.module('egCoreMod')
     var service = {
         field_doc : {},            // config.idl_field_doc
         profiles : [],             // permission groups
+        no_edit_profiles : [],     // perm groups we cannot modify
         sms_carriers : [],
         user_settings : {},        // applied user settings
         user_setting_types : {},   // config.usr_setting_type
@@ -32,6 +33,51 @@ angular.module('egCoreMod')
         ]);
     };
 
+    //service.check_grp_app_perm = function(grp_id) {
+
+    // determine which user groups our user is not allowed to modify
+    service.set_no_edit_profiles = function() {
+        var all_app_perms = [];
+        var failed_perms = [];
+
+        // extract the application permissions
+        angular.forEach(service.profiles, function(grp) {
+            if (grp.application_perm())
+                all_app_perms.push(grp.application_perm());
+        }); 
+
+        // fill in service.no_edit_profiles by inspecting failed_perms
+        function traverse_grp_tree(grp, failed) {
+            failed = failed || 
+                failed_perms.indexOf(grp.application_perm()) > -1;
+
+            if (failed) service.no_edit_profiles.push(grp.id());
+
+            angular.forEach(
+                service.profiles.filter( // children of grp
+                    function(p) { return p.parent() == grp.id() }),
+                function(child) {traverse_grp_tree(child, failed)}
+            );
+        }
+
+        return egCore.perm.hasPermAt(all_app_perms, true).then(
+            function(perm_orgs) {
+
+                angular.forEach(all_app_perms, function(p) {
+                    if (perm_orgs[p].length == 0)
+                        failed_perms.push(p);
+                });
+
+                traverse_grp_tree(egCore.env.pgt.tree);
+                console.log('User is not allowed to edit profiles: ' 
+                    + service.no_edit_profiles);
+            }
+        );
+    }
+
+    service.group_link_perms = function() {
+    }
+
     service.get_surveys = function() {
         var org_ids = egCore.org.fullPath(egCore.auth.user().ws_ou(), true);
 
@@ -221,7 +267,7 @@ angular.module('egCoreMod')
     service.get_perm_groups = function() {
         if (egCore.env.pgt) {
             service.profiles = egCore.env.pgt.list;
-            return $q.when();
+            return service.set_no_edit_profiles();
         } else {
             return egCore.pcrud.search('pgt', {parent : null}, 
                 {flesh : -1, flesh_fields : {pgt : ['children']}}
@@ -229,6 +275,7 @@ angular.module('egCoreMod')
                 function(tree) {
                     egCore.env.absorbTree(tree, 'pgt')
                     service.profiles = egCore.env.pgt.list;
+                    return service.set_no_edit_profiles();
                 }
             );
         }
@@ -345,6 +392,7 @@ angular.module('egCoreMod')
         patron.profile = current.profile(); // pre-hash version
         patron.net_access_level = current.net_access_level();
         patron.ident_type = current.ident_type();
+        patron.groups = current.groups(); // pre-hash
 
         angular.forEach(
             ['juvenile', 'barred', 'active', 'master_account'],
@@ -645,7 +693,6 @@ function PatronRegCtrl($scope, $routeParams,
             set_new_patron_defaults(prs);
 
         $scope.page_data_loaded = true;
-        console.log('here with ' + $scope.page_data_loaded);
     });
 
     // update the currently displayed field documentation
@@ -892,6 +939,43 @@ function PatronRegCtrl($scope, $routeParams,
         $scope.user_settings['opac.hold_notify'] = hold_notify;
     }
 
+    $scope.secondary_groups_dialog = function() {
+        $modal.open({
+            templateUrl: './circ/patron/t_patron_groups_dialog',
+            controller: 
+                   ['$scope','$modalInstance','linked_groups',
+            function($scope , $modalInstance , linked_groups) {
+                // scope here is the modal-level scope
+                $scope.link_group = function(grp) {
+                    $scope.args.linked_groups.push(grp);
+                    $event.preventDefault(); // avoid close
+                }
+                $scope.unlink_group = function($event, grp) {
+                    $scope.args.linked_groups = 
+                        $scope.args.linked_groups.filter(function(g) {
+                        return g.id() != grp.id()
+                    });
+                    $event.preventDefault(); // avoid close
+                }
+                $scope.args = {linked_groups : linked_groups};
+                $scope.ok = function() { $modalInstance.close($scope.args) }
+                $scope.cancel = function () { $modalInstance.dismiss() }
+            }],
+            resolve : {
+                linked_groups : function() {
+                    // scope here is the controller-level scope
+                    return $scope.patron.groups;
+                }
+            }
+        }).result.then(
+            function(args) {
+                angular.forEach(args.linked_groups, function(grp) {
+                    // TODO add/remove linked groups
+                });
+            }
+        );
+    }
+
     function extract_hold_notify() {
         notify = $scope.user_settings['opac.hold_notify'];
         if (!notify) return;

commit cf7766e55838f73e8c6538e1deddc84ee46ea0eb
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Nov 19 08:47:09 2015 -0500

    LP#1452950 Patron reg loading dialog; more caching
    
    * Hide the patron edit form and show a loading dialog while data loads.
    * Cache net access levels and ident types to speed up navigation between
      patron edit and other pages within the patron app.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 9caf5e7..a35e9e4 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -132,6 +132,22 @@
 </div>
 [% END %]
 
+<!-- progress dialog displayed as we await all data to finish loading -->
+<div class="row" ng-show="!page_data_loaded">
+  <div class="col-md-6 pad-vert">
+    <div class="progress progress-striped active">
+        <div class="progress-bar"  role="progressbar" aria-valuenow="100" 
+              aria-valuemin="0" aria-valuemax="100" style="width: 100%">
+            <span class="sr-only">[% l('Loading...') %]</span>
+        </div>
+    </div>
+  </div>
+</div>
+
+<!-- this div wraps the entire form so we can hide it 
+     until all needed data has been loaded -->
+<div ng-show="page_data_loaded"><!-- form wrapper -->
+
 [% formfield('ac', 'barcode', 'card') %]
 [% formfield('au', 'usrname') %]
 [% formfield('au', 'passwd') %]
@@ -494,4 +510,4 @@
   </div>
 </div>
 
-
+</div><!-- /form wrapper -->
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 8b67bee..dca92a8 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
@@ -193,13 +193,29 @@ angular.module('egCoreMod')
     };
 
     service.get_ident_types = function() {
-        return egCore.pcrud.retrieveAll('cit', {}, {atomic : true})
-        .then(function(types) { service.ident_types = types });
+        if (egCore.env.cit) {
+            service.ident_types = egCore.env.cit.list;
+            return $q.when();
+        } else {
+            return egCore.pcrud.retrieveAll('cit', {}, {atomic : true})
+            .then(function(types) { 
+                egCore.env.absorbList(types, 'cit')
+                service.ident_types = types 
+            });
+        }
     };
 
     service.get_net_access_levels = function() {
-        return egCore.pcrud.retrieveAll('cnal', {}, {atomic : true})
-        .then(function(levels) { service.net_access_levels = levels });
+        if (egCore.env.cnal) {
+            service.net_access_levels = egCore.env.cnal.list;
+            return $q.when();
+        } else {
+            return egCore.pcrud.retrieveAll('cnal', {}, {atomic : true})
+            .then(function(levels) { 
+                egCore.env.absorbList(levels, 'cnal')
+                service.net_access_levels = levels 
+            });
+        }
     }
 
     service.get_perm_groups = function() {
@@ -538,6 +554,7 @@ angular.module('egCoreMod')
 function PatronRegCtrl($scope, $routeParams, 
     $q, $modal, $window, egCore, patronSvc, patronRegSvc) {
 
+    $scope.page_data_loaded = false;
     $scope.clone_id = $routeParams.clone_id;
     $scope.stage_username = $routeParams.stage_username;
     $scope.patron_id = 
@@ -626,6 +643,9 @@ function PatronRegCtrl($scope, $routeParams,
 
         if ($scope.patron.isnew) 
             set_new_patron_defaults(prs);
+
+        $scope.page_data_loaded = true;
+        console.log('here with ' + $scope.page_data_loaded);
     });
 
     // update the currently displayed field documentation

commit 2dedeb342e9e84563d345e498b26bb6d22138095
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sat Oct 31 13:08:02 2015 -0400

    LP#1452950 patron reg code cleanup
    
    Break up one of the bigger chunks by moving new patron defaults to its
    own function.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 63ab0c4..8b67bee 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
@@ -558,6 +558,33 @@ function PatronRegCtrl($scope, $routeParams,
     $scope.edit_passthru.vis_level = 0; 
     // TODO: add save/clone handlers here
 
+    // Apply default values for new patrons during initial registration
+    // prs is shorthand for patronSvc
+    function set_new_patron_defaults(prs) {
+        $scope.generate_password();
+        $scope.hold_notify_phone = true;
+        $scope.hold_notify_email = true;
+
+        if (prs.org_settings['ui.patron.default_ident_type']) {
+            // $scope.patron needs this field to be an object
+            var id = prs.org_settings['ui.patron.default_ident_type'];
+            var ident_type = $scope.ident_types.filter(
+                function(type) { return type.id() == id })[0];
+            $scope.patron.ident_type = ident_type;
+        }
+        if (prs.org_settings['ui.patron.default_inet_access_level']) {
+            // $scope.patron needs this field to be an object
+            var id = prs.org_settings['ui.patron.default_inet_access_level'];
+            var level = $scope.net_access_levels.filter(
+                function(lvl) { return lvl.id() == id })[0];
+            $scope.patron.net_access_level = level;
+        }
+        if (prs.org_settings['ui.patron.default_country']) {
+            $scope.patron.addresses[0].country = 
+                prs.org_settings['ui.patron.default_country'];
+        }
+    }
+
     $q.all([
 
         $scope.initTab ? // initTab comes from patron app
@@ -597,31 +624,8 @@ function PatronRegCtrl($scope, $routeParams,
         if ($scope.org_settings['ui.patron.edit.default_suggested'])
             $scope.edit_passthru.vis_level = 1;
 
-        if ($scope.patron.isnew) {
-            $scope.generate_password();
-            $scope.hold_notify_phone = true;
-            $scope.hold_notify_email = true;
-
-            if (prs.org_settings['ui.patron.default_ident_type']) {
-                // $scope.patron needs this field to be an object
-                var id = prs.org_settings['ui.patron.default_ident_type'];
-                var ident_type = $scope.ident_types.filter(
-                    function(type) { return type.id() == id })[0];
-                $scope.patron.ident_type = ident_type;
-            }
-            if (prs.org_settings['ui.patron.default_inet_access_level']) {
-                // $scope.patron needs this field to be an object
-                var id = prs.org_settings['ui.patron.default_inet_access_level'];
-                var level = $scope.net_access_levels.filter(
-                    function(lvl) { return lvl.id() == id })[0];
-                $scope.patron.net_access_level = level;
-            }
-            if (prs.org_settings['ui.patron.default_country']) {
-                $scope.patron.addresses[0].country = 
-                    prs.org_settings['ui.patron.default_country'];
-            }
-        }
-            
+        if ($scope.patron.isnew) 
+            set_new_patron_defaults(prs);
     });
 
     // update the currently displayed field documentation

commit ae707cf1753728007e4f6811240e89cac9a9941c
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sat Oct 31 13:03:31 2015 -0400

    LP#1452950 Field doc display repair
    
    Set the current field doc via function instead of directly within the
    ng-click handler.  For unknown reasons, the direct approach was not
    working with addresses.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index f8186e4..9caf5e7 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -42,7 +42,7 @@
 
     <!-- field documentation img/link -->
     <img ng-show="field_doc.[% cls %].[% field %]" 
-      ng-click="selected_field_doc=field_doc.[% cls %].[% field %]"
+      ng-click="set_selected_field_doc('[% cls %]','[% field %]')"
       src='[% DOC_IMG %]'></img>
   </div>
 
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 298d8cc..63ab0c4 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
@@ -624,6 +624,11 @@ function PatronRegCtrl($scope, $routeParams,
             
     });
 
+    // update the currently displayed field documentation
+    $scope.set_selected_field_doc = function(cls, field) {
+        $scope.selected_field_doc = $scope.field_doc[cls][field];
+    }
+
     // returns the tree depth of the selected profile group tree node.
     $scope.pgt_depth = function(grp) {
         var d = 0;

commit 5ad63ee9e7c6490cc6600b5a1f1e32e373694518
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Oct 29 21:41:34 2015 -0400

    LP#1452950 survey dates/sorting; stat cat sorting
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 443c84c..298d8cc 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
@@ -33,11 +33,14 @@ angular.module('egCoreMod')
     };
 
     service.get_surveys = function() {
-        var org_ids = egCore.org.ancestors(egCore.auth.user().ws_ou(), true);
-
-        return egCore.pcrud.search('asv', 
-            {owner : org_ids}, 
-            {   flesh : 2, 
+        var org_ids = egCore.org.fullPath(egCore.auth.user().ws_ou(), true);
+
+        return egCore.pcrud.search('asv', {
+                owner : org_ids,
+                start_date : {'<=' : 'now'},
+                end_date : {'>=' : 'now'}
+            }, {   
+                flesh : 2, 
                 flesh_fields : {
                     asv : ['questions'], 
                     asvq : ['answers']
@@ -45,6 +48,8 @@ angular.module('egCoreMod')
             }, 
             {atomic : true}
         ).then(function(surveys) {
+            surveys = surveys.sort(function(a,b) {
+                return a.name() < b.name() ? -1 : 1 });
             service.surveys = surveys;
             angular.forEach(surveys, function(survey) {
                 angular.forEach(survey.questions(), function(question) {
@@ -63,6 +68,15 @@ angular.module('egCoreMod')
             'open-ils.circ.stat_cat.actor.retrieve.all',
             egCore.auth.token(), egCore.auth.user().ws_ou()
         ).then(function(cats) {
+            cats = cats.sort(function(a, b) {
+                return a.name() < b.name() ? -1 : 1});
+            angular.forEach(cats, function(cat) {
+                cat.entries(
+                    cat.entries().sort(function(a,b) {
+                        return a.value() < b.value() ? -1 : 1
+                    })
+                );
+            });
             service.stat_cats = cats;
         });
     };

commit 70b83d975b8d84e075d8e31858383e7f3d967f37
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Oct 25 22:24:16 2015 -0400

    LP#1452950 pat. reg surveys and stat cats
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 7f45948..f8186e4 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -441,12 +441,15 @@
     <div ng-if="cat.entries().length != 0">
       <div class="btn-group" dropdown>
         <button type="button" class="btn btn-default dropdown-toggle">
-          <span style="padding-right: 5px;"></span>
+          <span style="padding-right: 5px;">
+            {{stat_cat_entry_maps[cat.id()].value()}}</span>
           <span class="caret"></span>
         </button>
         <ul class="dropdown-menu">
           <li ng-repeat="entry in cat.entries()">
-            <a href ng-click=""> {{entry.value()}} </a>
+            <a href ng-click="stat_cat_entry_maps[cat.id()]=entry"> 
+              {{entry.value()}}
+            </a>
           </li>
         </ul>
       </div>
@@ -466,7 +469,7 @@
   <div class="col-md-3 reg-field-label">
     <label>{{survey.name()}}</label>
   </div>
-  <div class="col-md-6">
+  <div class="col-md-6 reg-field-input">
     <div class="row" ng-repeat="question in survey.questions()" 
       style="margin-bottom: 10px;">
       <div class="col-md-6">{{question.question()}}</div>
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 f848dbe..443c84c 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
@@ -14,7 +14,7 @@ angular.module('egCoreMod')
         survey_questions : {},
         survey_answers : {},
         survey_responses : {},     // survey.responses for loaded patron in progress
-        orig_survey_responses : {},// survey.responses for loaded patron
+        stat_cat_entry_maps : {},   // cat.id to selected entry object map
         virt_id : -1               // virtual ID for new objects
     };
 
@@ -46,7 +46,6 @@ angular.module('egCoreMod')
             {atomic : true}
         ).then(function(surveys) {
             service.surveys = surveys;
-
             angular.forEach(surveys, function(survey) {
                 angular.forEach(survey.questions(), function(question) {
                     service.survey_questions[question.id()] = question;
@@ -55,21 +54,6 @@ angular.module('egCoreMod')
                     });
                 });
             });
-
-            if (!service.patron_id) return;
-
-            // existing survey responses.
-            service.orig_survey_responses = [];
-
-            // when building responses in via the form, we track
-            // question ID's to answer objects.  these are later turned
-            // into survey_response's during save time.
-            return egCore.pcrud.search('asvr', {usr : service.patron_id})
-            .then(null, null, function(resp) {
-                service.orig_survey_responses.push(resp);
-                service.survey_responses[resp.question()] = 
-                    service.survey_answers[resp.answer()];
-            });
         });
     }
 
@@ -331,7 +315,6 @@ angular.module('egCoreMod')
         patron.profile = current.profile(); // pre-hash version
         patron.net_access_level = current.net_access_level();
         patron.ident_type = current.ident_type();
-        patron.survey_responses = service.orig_survey_responses;
 
         angular.forEach(
             ['juvenile', 'barred', 'active', 'master_account'],
@@ -349,6 +332,19 @@ angular.module('egCoreMod')
         angular.forEach(patron.addresses, 
             function(addr) { service.ingest_address(patron, addr) });
 
+        // toss entries for existing stat cat maps into our living 
+        // stat cat entry map, which is modified within the template.
+        angular.forEach(patron.stat_cat_entries, function(map) {
+            var entry;
+            angular.forEach(service.stat_cats, function(cat) {
+                angular.forEach(cat.entries(), function(ent) {
+                    if (ent.id() == map.stat_cat_entry)
+                        entry = ent;
+                });
+            });
+            service.stat_cat_entry_maps[map.stat_cat.id] = entry;
+        });
+
         return patron;
     }
 
@@ -360,7 +356,8 @@ angular.module('egCoreMod')
             address_type : egCore.strings.REG_ADDR_TYPE,
             _is_mailing : true,
             _is_billing : true,
-            within_city_limits : false
+            within_city_limits : false,
+            stat_cat_entries : []
         };
 
         var card = {
@@ -428,36 +425,57 @@ angular.module('egCoreMod')
             if (addr_hash._is_billing) patron.billing_address(addr);
         });
 
+        patron.survey_responses([]);
         angular.forEach(service.survey_responses, function(answer) {
+            var question = service.survey_questions[answer.question()];
+            var resp = new egCore.idl.asvr();
+            resp.isnew(true);
+            resp.survey(question.survey());
+            resp.question(question.id());
+            resp.answer(answer.id());
+            resp.usr(patron.id());
+            resp.answer_date('now');
+            patron.survey_responses().push(resp);
+        });
+        
+        // re-object-ify the patron stat cat entry maps
+        var maps = [];
+        angular.forEach(patron.stat_cat_entries(), function(entry) {
+            var e = egCore.idl.fromHash('actscecm', entry);
+            e.stat_cat(e.stat_cat().id);
+            maps.push(e);
+        });
+        patron.stat_cat_entries(maps);
 
-            var existing = patron.survey_responses().filter(
-                function(resp) {
-                    return resp.question() == answer.question();
-                })[0];
-
-            if (existing) {
-                if (existing.answer() != answer.id()) {
-                    // answer changed
-                    existing.answer(answer.id());
-                    existing.answer_date('now');
-                    existing.ischanged(true);
-                    existing.isnew(false);
-                    patron.survey_responses().push(existing);
-                }
-            } else {
-                // first-time answering this question
-
-                // find the question object linked to this answer
-                var question = service.survey_questions[answer.question()];
-                var resp = new egCore.idl.asvr();
-                resp.isnew(true);
-                resp.survey(question.survey());
-                resp.question(question.id());
-                resp.answer(answer.id());
-                resp.usr(patron.id());
-                resp.answer_date('now');
-                patron.survey_responses().push(resp);
+        // service.stat_cat_entry_maps maps stats to entries
+        // patron.stat_cat_entries is an array of stat_cat_entry_usr_map's
+        angular.forEach(service.stat_cat_entry_maps, function(entry) {
+
+            // see if we already have a mapping for this entry
+            var existing = patron.stat_cat_entries().filter(function(e) {
+                return e.stat_cat() == entry.stat_cat();
+            })[0];
+
+            if (existing) { // we have a mapping
+                // if the existing mapping matches the new one,
+                // there' nothing left to do
+                if (existing.stat_cat_entry() == entry.id()) return;
+
+                // mappings differ.  delete the old one and create
+                // a new one below.
+                existing.isdeleted(true);
             }
+
+            var newmap = new egCore.idl.actscecm();
+            newmap.target_usr(patron.id());
+            newmap.isnew(true);
+            newmap.stat_cat(entry.stat_cat());
+            newmap.stat_cat_entry(entry.id());
+            patron.stat_cat_entries().push(newmap);
+        });
+
+        angular.forEach(patron.stat_cat_entries(), function(entry) {
+            console.log(egCore.idl.toString(entry));
         });
 
         if (!patron.isnew()) patron.ischanged(true);
@@ -550,6 +568,7 @@ function PatronRegCtrl($scope, $routeParams,
         $scope.stat_cats = prs.stat_cats;
         $scope.surveys = prs.surveys;
         $scope.survey_responses = prs.survey_responses;
+        $scope.stat_cat_entry_maps = prs.stat_cat_entry_maps;
 
         $scope.user_settings = prs.user_settings;
         // clone the user settings back into the patronRegSvc so
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 d5d7def..7e45256 100644
--- a/Open-ILS/web/js/ui/default/staff/services/idl.js
+++ b/Open-ILS/web/js/ui/default/staff/services/idl.js
@@ -173,6 +173,19 @@ angular.module('egCoreMod')
         return hash;
     }
 
+    // returns a simple string key=value string of an IDL object.
+    service.toString = function(obj) {
+        var s = '';
+        angular.forEach(
+            service.classes[obj.classname].fields.sort(
+                function(a,b) {return a.name < b.name ? -1 : 1}),
+            function(field) {
+                s += field.name + '=' + obj[field.name]() + '\n';
+            }
+        );
+        return s;
+    }
+
     // hash-to-IDL object translater.  Does not support nested values.
     service.fromHash = function(cls, hash) {
         if (!service.classes[cls]) {

commit b284be7f2408d7d2682db280b6e1f9eb0a366ff8
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Oct 25 19:37:22 2015 -0400

    LP#1452950 pat. reg surveys and opt-in settings
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    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 981ccbb..ff9aa87 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -2298,7 +2298,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                 <create permission="ADMIN_SURVEY">
                     <context link="survey" field="owner"/>
                 </create>
-                <retrieve/>
+                <retrieve permission="VIEW_USER">
+                    <context link="usr" field="home_ou"/>
+                </retrieve>
                 <update permission="ADMIN_SURVEY">
                     <context link="survey" field="owner"/>
                 </update>
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index e6ffef4..7f45948 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -372,10 +372,17 @@
   </div>
 </div>
 
+<div class="row reg-field-row" ng-repeat="type in opt_in_setting_types">
+  <div class="col-md-3 reg-field-label">
+    <label>{{type.label()}}</label>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <input type='checkbox' ng-model="user_settings[type.name()]"/>
+  </div>
+</div>
 
 <!-- addresses -->
 
-
 <div ng-repeat="addr in patron.addresses">
   <div class="alert alert-success row" role="alert">
       <div class="col-md-3">[% l('Address') %]</div>
@@ -459,20 +466,29 @@
   <div class="col-md-3 reg-field-label">
     <label>{{survey.name()}}</label>
   </div>
-  <div class="col-md-3 reg-field-input">
-    <div class="btn-group" dropdown>
-      <button type="button" class="btn btn-default dropdown-toggle">
-        <span style="padding-right: 5px;"></span>
-        <span class="caret"></span>
-      </button>
-      <ul class="dropdown-menu">
-        <li ng-repeat="question in survey.questions()">
-          <a href ng-click=""> {{question.question()}} </a>
-        </li>
-      </ul>
+  <div class="col-md-6">
+    <div class="row" ng-repeat="question in survey.questions()" 
+      style="margin-bottom: 10px;">
+      <div class="col-md-6">{{question.question()}}</div>
+      <div class="col-md-6">
+        <div class="btn-group" dropdown>
+          <button type="button" class="btn btn-default dropdown-toggle">
+            <span style="padding-right: 5px;">
+              {{survey_responses[question.id()].answer()}}
+            </span>
+            <span class="caret"></span>
+          </button>
+          <ul class="dropdown-menu">
+            <li ng-repeat="answer in question.answers()">
+              <a href ng-click="survey_responses[question.id()] = answer"> 
+                {{answer.answer()}} 
+              </a>
+            </li>
+          </ul>
+        </div>
+      </div>
     </div>
   </div>
 </div>
 
 
-
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 52dd1f8..f848dbe 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
@@ -5,12 +5,17 @@ angular.module('egCoreMod')
 .factory('patronRegSvc', ['$q', 'egCore', function($q, egCore) {
 
     var service = {
-        field_doc : {},              // config.idl_field_doc
-        profiles : [],               // permission groups
+        field_doc : {},            // config.idl_field_doc
+        profiles : [],             // permission groups
         sms_carriers : [],
-        user_settings : {},          // applied user settings
-        user_setting_types : {},     // config.usr_setting_type
-        virt_id : -1                 // virtual ID for new objects
+        user_settings : {},        // applied user settings
+        user_setting_types : {},   // config.usr_setting_type
+        opt_in_setting_types : {}, // config.usr_setting_type for event-def opt-in
+        survey_questions : {},
+        survey_answers : {},
+        survey_responses : {},     // survey.responses for loaded patron in progress
+        orig_survey_responses : {},// survey.responses for loaded patron
+        virt_id : -1               // virtual ID for new objects
     };
 
     // launch a series of parallel data retrieval calls
@@ -32,10 +37,39 @@ angular.module('egCoreMod')
 
         return egCore.pcrud.search('asv', 
             {owner : org_ids}, 
-            {flesh : 1, flesh_fields : {asv : ['questions']}}, 
+            {   flesh : 2, 
+                flesh_fields : {
+                    asv : ['questions'], 
+                    asvq : ['answers']
+                }
+            }, 
             {atomic : true}
         ).then(function(surveys) {
             service.surveys = surveys;
+
+            angular.forEach(surveys, function(survey) {
+                angular.forEach(survey.questions(), function(question) {
+                    service.survey_questions[question.id()] = question;
+                    angular.forEach(question.answers(), function(answer) {
+                        service.survey_answers[answer.id()] = answer;
+                    });
+                });
+            });
+
+            if (!service.patron_id) return;
+
+            // existing survey responses.
+            service.orig_survey_responses = [];
+
+            // when building responses in via the form, we track
+            // question ID's to answer objects.  these are later turned
+            // into survey_response's during save time.
+            return egCore.pcrud.search('asvr', {usr : service.patron_id})
+            .then(null, null, function(resp) {
+                service.orig_survey_responses.push(resp);
+                service.survey_responses[resp.question()] = 
+                    service.survey_answers[resp.answer()];
+            });
         });
     }
 
@@ -201,16 +235,18 @@ angular.module('egCoreMod')
     service.get_user_settings = function() {
         var org_ids = egCore.org.ancestors(egCore.auth.user().ws_ou(), true);
 
+        var static_types = [
+            'circ.holds_behind_desk', 
+            'circ.collections.exempt', 
+            'opac.hold_notify', 
+            'opac.default_phone', 
+            'opac.default_pickup_location', 
+            'opac.default_sms_carrier', 
+            'opac.default_sms_notify'];
+
         return egCore.pcrud.search('cust', {
             '-or' : [
-                {name : [ // common user settings
-                    'circ.holds_behind_desk', 
-                    'circ.collections.exempt', 
-                    'opac.hold_notify', 
-                    'opac.default_phone', 
-                    'opac.default_pickup_location', 
-                    'opac.default_sms_carrier', 
-                    'opac.default_sms_notify']}, 
+                {name : static_types}, // common user settings
                 {name : { // opt-in notification user settings
                     'in': {
                         select : {atevdef : ['opt_in_setting']}, 
@@ -225,6 +261,9 @@ angular.module('egCoreMod')
 
             angular.forEach(setting_types, function(stype) {
                 service.user_setting_types[stype.name()] = stype;
+                if (static_types.indexOf(stype.name()) == -1) {
+                    service.opt_in_setting_types[stype.name()] = stype;
+                }
             });
 
             if (service.patron_id) {
@@ -292,6 +331,7 @@ angular.module('egCoreMod')
         patron.profile = current.profile(); // pre-hash version
         patron.net_access_level = current.net_access_level();
         patron.ident_type = current.ident_type();
+        patron.survey_responses = service.orig_survey_responses;
 
         angular.forEach(
             ['juvenile', 'barred', 'active', 'master_account'],
@@ -386,11 +426,39 @@ angular.module('egCoreMod')
             addr.within_city_limits(addr.within_city_limits() ? 't' : 'f');
             if (addr_hash._is_mailing) patron.mailing_address(addr);
             if (addr_hash._is_billing) patron.billing_address(addr);
-
-            console.log('deleted? ' + addr.isdeleted());
         });
 
-        console.log(patron.addresses());
+        angular.forEach(service.survey_responses, function(answer) {
+
+            var existing = patron.survey_responses().filter(
+                function(resp) {
+                    return resp.question() == answer.question();
+                })[0];
+
+            if (existing) {
+                if (existing.answer() != answer.id()) {
+                    // answer changed
+                    existing.answer(answer.id());
+                    existing.answer_date('now');
+                    existing.ischanged(true);
+                    existing.isnew(false);
+                    patron.survey_responses().push(existing);
+                }
+            } else {
+                // first-time answering this question
+
+                // find the question object linked to this answer
+                var question = service.survey_questions[answer.question()];
+                var resp = new egCore.idl.asvr();
+                resp.isnew(true);
+                resp.survey(question.survey());
+                resp.question(question.id());
+                resp.answer(answer.id());
+                resp.usr(patron.id());
+                resp.answer_date('now');
+                patron.survey_responses().push(resp);
+            }
+        });
 
         if (!patron.isnew()) patron.ischanged(true);
 
@@ -476,10 +544,12 @@ function PatronRegCtrl($scope, $routeParams,
         $scope.ident_types = prs.ident_types;
         $scope.net_access_levels = prs.net_access_levels;
         $scope.user_setting_types = prs.user_setting_types;
+        $scope.opt_in_setting_types = prs.opt_in_setting_types;
         $scope.org_settings = prs.org_settings;
         $scope.sms_carriers = prs.sms_carriers;
         $scope.stat_cats = prs.stat_cats;
         $scope.surveys = prs.surveys;
+        $scope.survey_responses = prs.survey_responses;
 
         $scope.user_settings = prs.user_settings;
         // clone the user settings back into the patronRegSvc so
@@ -500,12 +570,18 @@ function PatronRegCtrl($scope, $routeParams,
             $scope.hold_notify_email = true;
 
             if (prs.org_settings['ui.patron.default_ident_type']) {
-                $scope.patron.ident_type = 
-                    prs.org_settings['ui.patron.default_ident_type'];
+                // $scope.patron needs this field to be an object
+                var id = prs.org_settings['ui.patron.default_ident_type'];
+                var ident_type = $scope.ident_types.filter(
+                    function(type) { return type.id() == id })[0];
+                $scope.patron.ident_type = ident_type;
             }
             if (prs.org_settings['ui.patron.default_inet_access_level']) {
-                $scope.patron.ident_type = 
-                    prs.org_settings['ui.patron.default_inet_access_level'];
+                // $scope.patron needs this field to be an object
+                var id = prs.org_settings['ui.patron.default_inet_access_level'];
+                var level = $scope.net_access_levels.filter(
+                    function(lvl) { return lvl.id() == id })[0];
+                $scope.patron.net_access_level = level;
             }
             if (prs.org_settings['ui.patron.default_country']) {
                 $scope.patron.addresses[0].country = 

commit bb0c48a7e27e1b59cf5bd6ab3f2c10ae4d03f04a
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Oct 25 11:42:11 2015 -0400

    LP#1452950 patron reg additions (phone pw, hiding stuff)
    
    1. Generate password from last 4 digits of phone number if library
    setting is present (patron.password.use_phone).
    
    2. Only show "replace barcode" and invlidate buttons when editing an
    existing patron.
    
    3. Address no longer defaults to within_city_limits, consistent with
    dojo version.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 2d8e8c3..e6ffef4 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -75,6 +75,10 @@
         focus-me="focus_usrname"
         ng-blur="usrname_changed(patron.usrname)"
         class="form-control" ng-model="[% model %]"/>
+    [% ELSIF field == 'day_phone' %]
+      <input type="text" 
+        ng-blur="day_phone_changed(patron.day_phone)"
+        class="form-control" ng-model="[% model %]"/>
     [% ELSE %]
       <input type="[% input_type %]" 
         class="form-control" ng-model="[% model %]"/>
@@ -88,7 +92,7 @@
 
     [% IF field == 'barcode' %]
 
-      <button class="btn btn-default"
+      <button class="btn btn-default" ng-show="!patron.isnew"
         ng-click="replace_card()">[% l('Replace Barcode') %]</button>
       <button class="btn btn-default" 
         ng-click="cards_dialog()">[% l('See All') %]</button>
@@ -103,7 +107,7 @@
       <!-- invalidate buttons -->
 
       [% IF field.match('phone') OR field.match('email') %]
-        <button ng-show="patron.[% field %]" class="btn btn-default"
+        <button ng-show="patron.[% field %] && !patron.isnew" class="btn btn-default"
           ng-click="">[% l('Invalidate') %]</button>
       [% END %]
 
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 40809e5..52dd1f8 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
@@ -320,7 +320,7 @@ angular.module('egCoreMod')
             address_type : egCore.strings.REG_ADDR_TYPE,
             _is_mailing : true,
             _is_billing : true,
-            within_city_limits : true
+            within_city_limits : false
         };
 
         var card = {
@@ -659,6 +659,12 @@ function PatronRegCtrl($scope, $routeParams,
         $scope.patron.cards.push(new_card);
     }
 
+    $scope.day_phone_changed = function(phone) {
+        if (phone && $scope.org_settings['patron.password.use_phone']) {
+           $scope.patron.passwd = phone.substr(-4);
+        }
+    }
+
     $scope.barcode_changed = function(bc) {
         if (!bc) return;
         egCore.net.request(

commit 9d0fa75d93e732f89c175ee6bd19e7a301f06bf9
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Oct 4 20:58:35 2015 -0400

    LP#1452950 focus barcode field
    
    For new patrons, focus the barcode field.  For existing patrons, disable
    the barcode field (except when a new barcode is needed) and focus the
    username field by default.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 1e7cc71..2d8e8c3 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -65,7 +65,15 @@
       <input type="text" ng-blur="post_code_changed(patron.[% path %])"
         class="form-control" ng-model="[% model %]"/>
     [% ELSIF field == 'barcode' %]
-      <input type="text" ng-blur="barcode_changed(patron.card.barcode)"
+      <input type="text" 
+        focus-me="focus_bc"
+        ng-disabled="disable_bc"
+        ng-blur="barcode_changed(patron.card.barcode)"
+        class="form-control" ng-model="[% model %]"/>
+    [% ELSIF field == 'usrname' %]
+      <input type="text" 
+        focus-me="focus_usrname"
+        ng-blur="usrname_changed(patron.usrname)"
         class="form-control" ng-model="[% model %]"/>
     [% ELSE %]
       <input type="[% input_type %]" 
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 91ea251..40809e5 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
@@ -443,6 +443,10 @@ function PatronRegCtrl($scope, $routeParams,
     $scope.patron_id = 
         patronRegSvc.patron_id = $routeParams.edit_id || $routeParams.id;
 
+    // for existing patrons, disable barcode input by default
+    $scope.disable_bc = $scope.focus_usrname = Boolean($scope.patron_id);
+    $scope.focus_bc = !Boolean($scope.patron_id);
+
     if (!$scope.edit_passthru) {
         // in edit more, scope.edit_passthru is delivered to us by
         // the enclosing controller.  In register mode, there is 
@@ -644,6 +648,7 @@ function PatronRegCtrl($scope, $routeParams,
     $scope.replace_card = function() {
         $scope.patron.card.active = false;
         $scope.patron.card.ischanged = true;
+        $scope.disable_bc = false;
 
         var new_card = egCore.idl.toHash(new egCore.idl.ac());
         new_card.id = patronRegSvc.virt_id--;

commit 6319263f8356e5095c6ace3e36d9f33f2f79b1da
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Sep 20 10:35:51 2015 -0400

    LP#1452950 username defaults to barcode
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 9e3cb29..91ea251 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
@@ -665,6 +665,8 @@ function PatronRegCtrl($scope, $routeParams,
                 console.log('duplicate barcode detected: ' + bc);
                 // DUPLICATE CARD
             } else {
+                if (!$scope.patron.usrname)
+                    $scope.patron.usrname = bc;
                 // No dupe -- A-OK
             }
         });

commit 5587c8cd022fb5b83c19446686385699f97b722e
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Sep 20 10:29:53 2015 -0400

    LP#1452950 save/delete/modify addresses
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 291095e..1e7cc71 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -369,10 +369,14 @@
       <div class="col-md-3">[% l('Address') %]</div>
       <div class="col-md-3">
           <span class='pad-all-min'>
-            [% l('Mailing') %] <input type='checkbox' ng-model="addr._is_mailing"/>
+            [% l('Mailing') %] <input type='checkbox' 
+              ng-change="set_addr_type(addr, 'mailing')" 
+              ng-model="addr._is_mailing"/>
           </span>
           <span class='pad-all-min'>
-            [% l('Physical') %] <input type='checkbox' ng-model="addr._is_billing"/>
+            [% l('Physical') %] <input type='checkbox' 
+              ng-change="set_addr_type(addr, 'billing')" 
+              ng-model="addr._is_billing"/>
           </span>
           <span class='pad-all-min'>
             <button type="button" ng-click="delete_address(addr.id)" 
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 c68b4c4..9e3cb29 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
@@ -345,12 +345,7 @@ angular.module('egCoreMod')
     // translate the patron back into IDL form
     service.save_user = function(phash) {
 
-        var patron = new egCore.idl.au();
-
-        for (var key in phash) {
-            if (typeof patron[key] == 'function')
-                patron[key](phash[key]);
-        }
+        var patron = egCore.idl.fromHash('au', phash);
 
         patron.home_ou(patron.home_ou().id());
         patron.expire_date(
@@ -371,15 +366,10 @@ angular.module('egCoreMod')
         var card_hashes = patron.cards();
         patron.cards([]);
         angular.forEach(card_hashes, function(chash) {
-            var card = new egCore.idl.ac();
-            for (var key in chash) {
-                if (typeof card[key] == 'function') 
-                    card[key](chash[key]);
-            }
+            var card = egCore.idl.fromHash('ac', chash)
             card.usr(patron.id());
             card.active(chash.active ? 't' : 'f');
             patron.cards().push(card);
-
             if (chash._primary) {
                 patron.card(card);
             }
@@ -388,20 +378,19 @@ angular.module('egCoreMod')
         var addr_hashes = patron.addresses();
         patron.addresses([]);
         angular.forEach(addr_hashes, function(addr_hash) {
-            var addr = new egCore.idl.aua();
+            if (!addr_hash.isnew && !addr_hash.isdeleted) 
+                addr_hash.ischanged = true;
+            var addr = egCore.idl.fromHash('aua', addr_hash);
             patron.addresses().push(addr);
-            for (var key in addr_hash) {
-                if (typeof addr[key] == 'function') 
-                    addr[key](addr_hash[key]);
-            }
-
             addr.valid(addr.valid() ? 't' : 'f');
             addr.within_city_limits(addr.within_city_limits() ? 't' : 'f');
             if (addr_hash._is_mailing) patron.mailing_address(addr);
             if (addr_hash._is_billing) patron.billing_address(addr);
+
+            console.log('deleted? ' + addr.isdeleted());
         });
 
-        // TODO extract hold_notify_phone, etc.
+        console.log(patron.addresses());
 
         if (!patron.isnew()) patron.ischanged(true);
 
@@ -419,7 +408,6 @@ angular.module('egCoreMod')
         if (service.patron_id) {
             // only update modified settings for existing patrons
             angular.forEach(user_settings, function(val, key) {
-                console.log(val + ' : ' + service.user_settings[key]);
                 if (val !== service.user_settings[key])
                     settings[key] = val;
             });
@@ -431,8 +419,6 @@ angular.module('egCoreMod')
             });
         }
 
-        console.log('updating settings ' + Object.keys(settings));
-
         if (Object.keys(settings).length == 0) return $q.when();
 
         return egCore.net.request(
@@ -441,6 +427,7 @@ angular.module('egCoreMod')
             egCore.auth.token(), new_user.id(), settings
         ).then(function(resp) {
             console.log('settings returned ' + resp);
+            return resp;
         });
     }
 
@@ -617,6 +604,7 @@ function PatronRegCtrl($scope, $routeParams,
         var addr = egCore.idl.toHash(new egCore.idl.aua());
         patronRegSvc.ingest_address($scope.patron, addr);
         addr.id = patronRegSvc.virt_id--;
+        addr.isnew = true;
         addr.valid = true;
         addr.within_city_limits = true;
         $scope.patron.addresses.push(addr);
@@ -712,6 +700,27 @@ function PatronRegCtrl($scope, $routeParams,
         );
     }
 
+    $scope.set_addr_type = function(addr, type) {
+        var addrs = $scope.patron.addresses;
+        if (addr['_is_'+type]) {
+            angular.forEach(addrs, function(a) {
+                if (a.id != addr.id) a['_is_'+type] = false;
+            });
+        } else {
+            // unchecking mailing/billing means we have to randomly
+            // select another address to fill that role.  Select the
+            // first address in the list (that does not match the
+            // modifed address)
+            for (var i = 0; i < addrs.length; i++) {
+                if (addrs[i].id != addr.id) {
+                    addrs[i]['_is_' + type] = true;
+                    break;
+                }
+            }
+        }
+    }
+
+
     // Translate hold notify preferences from the form/scope back into a 
     // single user setting value for opac.hold_notify.
     function compress_hold_notify() {
@@ -741,12 +750,24 @@ function PatronRegCtrl($scope, $routeParams,
     }
 
     $scope.edit_passthru.save = function() {
+
+        // toss the deleted addresses back into the patron's list of
+        // addresses so it's included in the update
+        $scope.patron.addresses = 
+            $scope.patron.addresses.concat(deleted_addresses);
+        
         compress_hold_notify();
+
         patronRegSvc.save_user($scope.patron)
         .then(function(new_user) { 
-            return patronRegSvc.save_user_settings(
-                new_user, $scope.user_settings); 
-        }).then(function() {
+            if (new_user && new_user.classname) {
+                return patronRegSvc.save_user_settings(
+                    new_user, $scope.user_settings); 
+            } else {
+                alert('Patron update failed. \n\n' + js2JSON(new_user));
+                return true; // ensure page reloads to reset
+            }
+        }).then(function(keep_going) {
             // 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.
@@ -755,5 +776,8 @@ function PatronRegCtrl($scope, $routeParams,
     }
 }
 
+// This controller may be loaded from different modules (patron edit vs.
+// register new patron), so we have to inject the controller params manually.
+PatronRegCtrl.$inject = ['$scope', '$routeParams', '$q', '$modal', 
+    '$window', 'egCore', 'patronSvc', 'patronRegSvc'];
 
-// TODO: $inject controller params 
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 fda58d3..d5d7def 100644
--- a/Open-ILS/web/js/ui/default/staff/services/idl.js
+++ b/Open-ILS/web/js/ui/default/staff/services/idl.js
@@ -173,6 +173,22 @@ angular.module('egCoreMod')
         return hash;
     }
 
+    // hash-to-IDL object translater.  Does not support nested values.
+    service.fromHash = function(cls, hash) {
+        if (!service.classes[cls]) {
+            console.error('No such IDL class ' + cls);
+            return null;
+        }
+
+        var new_obj = new service[cls]();
+        angular.forEach(hash, function(val, key) {
+            if (!angular.isFunction(new_obj[key])) return;
+            new_obj[key](hash[key]);
+        });
+
+        return new_obj;
+    }
+
     // Transforms a flattened hash (see toHash() or egGridFlatDataProvider)
     // to a nested hash.
     //

commit 3a5cd43d7b9b320b300bc33c7bdc37ded291b58f
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Sep 20 09:33:12 2015 -0400

    LP#1452950 extract and save patron settings
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index d70c60f..291095e 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -322,7 +322,7 @@
       <input type='checkbox' ng-model="hold_notify_email"/>
       [% l('Email') %]
     </div>
-    <div class='flex-cell'>
+    <div class='flex-cell' ng-if="org_settings['sms.enable']">
       <input type='checkbox' ng-model="hold_notify_sms"/>
       [% l('SMS') %]
     </div>
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 91061b9..c68b4c4 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
@@ -10,7 +10,6 @@ angular.module('egCoreMod')
         sms_carriers : [],
         user_settings : {},          // applied user settings
         user_setting_types : {},     // config.usr_setting_type
-        modified_user_settings : {}, // settings modifed this session
         virt_id : -1                 // virtual ID for new objects
     };
 
@@ -344,7 +343,7 @@ angular.module('egCoreMod')
     }
 
     // translate the patron back into IDL form
-    service.save_patron = function(phash) {
+    service.save_user = function(phash) {
 
         var patron = new egCore.idl.au();
 
@@ -406,15 +405,42 @@ angular.module('egCoreMod')
 
         if (!patron.isnew()) patron.ischanged(true);
 
-        console.log(js2JSON(patron)); // TODO: debugging
-
-        egCore.net.request(
+        return egCore.net.request(
             'open-ils.actor', 
             'open-ils.actor.patron.update',
-            egCore.auth.token(), patron)
-        .then(function(resp) {
-            // TODO: see original
-            console.log(js2JSON(resp));
+            egCore.auth.token(), patron);
+    }
+
+    service.save_user_settings = function(new_user, user_settings) {
+        // user_settings contains the values from the scope/form.
+        // service.user_settings contain the values from page load time.
+
+        var settings = {};
+        if (service.patron_id) {
+            // only update modified settings for existing patrons
+            angular.forEach(user_settings, function(val, key) {
+                console.log(val + ' : ' + service.user_settings[key]);
+                if (val !== service.user_settings[key])
+                    settings[key] = val;
+            });
+
+        } else {
+            // all non-null setting values are updated for new patrons
+            angular.forEach(user_settings, function(val, key) {
+                if (val !== null) settings[key] = val;
+            });
+        }
+
+        console.log('updating settings ' + Object.keys(settings));
+
+        if (Object.keys(settings).length == 0) return $q.when();
+
+        return egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.patron.settings.update',
+            egCore.auth.token(), new_user.id(), settings
+        ).then(function(resp) {
+            console.log('settings returned ' + resp);
         });
     }
 
@@ -423,7 +449,7 @@ angular.module('egCoreMod')
 
 
 function PatronRegCtrl($scope, $routeParams, 
-    $q, $modal, egCore, patronSvc, patronRegSvc) {
+    $q, $modal, $window, egCore, patronSvc, patronRegSvc) {
 
     $scope.clone_id = $routeParams.clone_id;
     $scope.stage_username = $routeParams.stage_username;
@@ -458,14 +484,22 @@ function PatronRegCtrl($scope, $routeParams,
         $scope.profiles = prs.profiles;
         $scope.ident_types = prs.ident_types;
         $scope.net_access_levels = prs.net_access_levels;
-        $scope.user_settings = prs.user_settings;
         $scope.user_setting_types = prs.user_setting_types;
-        $scope.modified_user_settings = prs.modified_user_settings;
         $scope.org_settings = prs.org_settings;
         $scope.sms_carriers = prs.sms_carriers;
         $scope.stat_cats = prs.stat_cats;
         $scope.surveys = prs.surveys;
 
+        $scope.user_settings = prs.user_settings;
+        // clone the user settings back into the patronRegSvc so
+        // we have a copy of the original state of the settings.
+        prs.user_settings = {};
+        angular.forEach($scope.user_settings, function(val, key) {
+            prs.user_settings[key] = val;
+        });
+
+        extract_hold_notify();
+
         if ($scope.org_settings['ui.patron.edit.default_suggested'])
             $scope.edit_passthru.vis_level = 1;
 
@@ -678,10 +712,47 @@ function PatronRegCtrl($scope, $routeParams,
         );
     }
 
-    $scope.edit_passthru.save = function() {
-        patronRegSvc.save_patron($scope.patron);        
+    // Translate hold notify preferences from the form/scope back into a 
+    // single user setting value for opac.hold_notify.
+    function compress_hold_notify() {
+        var hold_notify = '';
+        var splitter = '';
+        if ($scope.hold_notify_phone) {
+            hold_notify = 'phone';
+            splitter = ':';
+        }
+        if ($scope.hold_notify_email) {
+            hold_notify = splitter + 'email';
+            splitter = ':';
+        }
+        if ($scope.hold_notify_sms) {
+            hold_notify = splitter + 'sms';
+            splitter = ':';
+        }
+        $scope.user_settings['opac.hold_notify'] = hold_notify;
+    }
+
+    function extract_hold_notify() {
+        notify = $scope.user_settings['opac.hold_notify'];
+        if (!notify) return;
+        $scope.hold_notify_phone = Boolean(notify.match(/phone/));
+        $scope.hold_notify_email = Boolean(notify.match(/email/));
+        $scope.hold_notify_sms = Boolean(notify.match(/sms/));
     }
 
+    $scope.edit_passthru.save = function() {
+        compress_hold_notify();
+        patronRegSvc.save_user($scope.patron)
+        .then(function(new_user) { 
+            return patronRegSvc.save_user_settings(
+                new_user, $scope.user_settings); 
+        }).then(function() {
+            // 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.
+            $window.location.href = location.href;
+        });
+    }
 }
 
 

commit 425a295fdcd5210f1f313e1491f6e6f0885bcc97
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Sep 17 21:51:54 2015 -0400

    LP#1452950 patron reg i18n repair; more saving
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/register.tt2 b/Open-ILS/src/templates/staff/circ/patron/register.tt2
index 96332d2..81b4279 100644
--- a/Open-ILS/src/templates/staff/circ/patron/register.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/register.tt2
@@ -9,6 +9,11 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/date.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/register.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/regctl.js"></script>
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+  s.REG_ADDR_TYPE = "[% l('Mailing') %]";
+}]);
+</script>
 <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/circ.css" />
 <style>
   /* add room for the fixed navigation elements */
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 1c29a2b..91061b9 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
@@ -314,12 +314,11 @@ angular.module('egCoreMod')
     }
 
     service.init_new_patron = function() {
-
         var addr = {
             id : service.virt_id--,
             isnew : true,
             valid : true,
-            address_type : 'MAILING', // TODO: i18n
+            address_type : egCore.strings.REG_ADDR_TYPE,
             _is_mailing : true,
             _is_billing : true,
             within_city_limits : true
@@ -405,7 +404,9 @@ angular.module('egCoreMod')
 
         // TODO extract hold_notify_phone, etc.
 
-        console.log(js2JSON(patron));
+        if (!patron.isnew()) patron.ischanged(true);
+
+        console.log(js2JSON(patron)); // TODO: debugging
 
         egCore.net.request(
             'open-ils.actor', 

commit d40fb3479d730662bf6bf37d4401a175a01054a6
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Sep 17 21:33:11 2015 -0400

    LP#1452950 patron reg saving new patrons repairs
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 6c80983..1c29a2b 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
@@ -5,12 +5,13 @@ angular.module('egCoreMod')
 .factory('patronRegSvc', ['$q', 'egCore', function($q, egCore) {
 
     var service = {
-        field_doc : {},             // config.idl_field_doc
-        profiles : [],              // permission groups
+        field_doc : {},              // config.idl_field_doc
+        profiles : [],               // permission groups
         sms_carriers : [],
-        user_settings : {},         // applied user settings
-        user_setting_types : {},    // config.usr_setting_type
-        modified_user_settings : {} // settings modifed this session
+        user_settings : {},          // applied user settings
+        user_setting_types : {},     // config.usr_setting_type
+        modified_user_settings : {}, // settings modifed this session
+        virt_id : -1                 // virtual ID for new objects
     };
 
     // launch a series of parallel data retrieval calls
@@ -315,20 +316,30 @@ angular.module('egCoreMod')
     service.init_new_patron = function() {
 
         var addr = {
+            id : service.virt_id--,
+            isnew : true,
             valid : true,
             address_type : 'MAILING', // TODO: i18n
+            _is_mailing : true,
+            _is_billing : true,
             within_city_limits : true
-            // default state, etc.
+        };
+
+        var card = {
+            id : service.virt_id--,
+            isnew : true,
+            active : true,
+            _primary : 'on'
         };
 
         return {
             isnew : true,
             active : true,
-            card : {},
+            card : card,
+            cards : [card],
             home_ou : egCore.org.get(egCore.auth.user().ws_ou()),
                         
             // TODO default profile group?
-            mailing_address : addr,
             addresses : [addr]
         };
     }
@@ -346,10 +357,13 @@ angular.module('egCoreMod')
         patron.home_ou(patron.home_ou().id());
         patron.expire_date(
             patron.expire_date().toISOString().replace(/T.*/,''));
-        patron.dob(patron.dob().toISOString().replace(/T.*/,''));
         patron.profile(patron.profile().id());
-        patron.net_access_level(patron.net_access_level().id());
-        patron.ident_type(patron.ident_type().id());
+        if (patron.dob()) 
+            patron.dob(patron.dob().toISOString().replace(/T.*/,''));
+        if (patron.ident_type()) 
+            patron.ident_type(patron.ident_type().id());
+        if (patron.net_access_level())
+            patron.net_access_level(patron.net_access_level().id());
 
         angular.forEach(
             ['juvenile', 'barred', 'active', 'master_account'],
@@ -391,6 +405,8 @@ angular.module('egCoreMod')
 
         // TODO extract hold_notify_phone, etc.
 
+        console.log(js2JSON(patron));
+
         egCore.net.request(
             'open-ils.actor', 
             'open-ils.actor.patron.update',
@@ -399,8 +415,6 @@ angular.module('egCoreMod')
             // TODO: see original
             console.log(js2JSON(resp));
         });
-
-        console.log(js2JSON(patron));
     }
 
     return service;
@@ -564,11 +578,10 @@ function PatronRegCtrl($scope, $routeParams,
         $scope.set_expire_date();
     }
 
-    var new_addr_id = -1;
     $scope.new_address = function() {
         var addr = egCore.idl.toHash(new egCore.idl.aua());
         patronRegSvc.ingest_address($scope.patron, addr);
-        addr.id = new_addr_id--;
+        addr.id = patronRegSvc.virt_id--;
         addr.valid = true;
         addr.within_city_limits = true;
         $scope.patron.addresses.push(addr);
@@ -605,13 +618,12 @@ function PatronRegCtrl($scope, $routeParams,
         });
     }
 
-    var new_card_id = -1;
     $scope.replace_card = function() {
         $scope.patron.card.active = false;
         $scope.patron.card.ischanged = true;
 
         var new_card = egCore.idl.toHash(new egCore.idl.ac());
-        new_card.id = new_card_id--;
+        new_card.id = patronRegSvc.virt_id--;
         new_card.isnew = true;
         new_card.active = true;
         new_card._primary = 'on';

commit 71e317777d9def5446c37d707ba6c42b106779fa
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Sep 17 20:50:07 2015 -0400

    LP#1452950 patron reg new patron defaults
    
    Additions and repairs for default user setting values, addr type,
    password, net access level, ident type, state.
    
    Also includes repairs for barcode replacements.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 d9cd490..6c80983 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
@@ -227,7 +227,7 @@ angular.module('egCoreMod')
                 service.user_setting_types[stype.name()] = stype;
             });
 
-            if(service.patron_id) {
+            if (service.patron_id) {
                 // retrieve applied values for the current user 
                 // for the setting types we care about.
 
@@ -243,16 +243,16 @@ angular.module('egCoreMod')
                 ).then(function(settings) {
                     service.user_settings = settings;
                 });
-            }
+            } else {
 
-            // apply default user setting values
-            angular.forEach(setting_types, function(stype, index) {
-                if (stype.reg_default() != undefined) {
-                    service.modified_user_settings[setting.name()] = 
+                // apply default user setting values
+                angular.forEach(setting_types, function(stype, index) {
+                    if (stype.reg_default() != undefined) {
                         service.user_settings[setting.name()] = 
-                        setting.reg_default();
-                }
-            });
+                            setting.reg_default();
+                    }
+                });
+            }
         });
     }
 
@@ -300,8 +300,10 @@ angular.module('egCoreMod')
 
         angular.forEach(patron.cards, function(card) {
             card.active = card.active == 't';
-            if (card.id == patron.card.id)
+            if (card.id == patron.card.id) {
+                patron.card = card;
                 card._primary = 'on';
+            }
         });
 
         angular.forEach(patron.addresses, 
@@ -314,6 +316,7 @@ angular.module('egCoreMod')
 
         var addr = {
             valid : true,
+            address_type : 'MAILING', // TODO: i18n
             within_city_limits : true
             // default state, etc.
         };
@@ -323,6 +326,7 @@ angular.module('egCoreMod')
             active : true,
             card : {},
             home_ou : egCore.org.get(egCore.auth.user().ws_ou()),
+                        
             // TODO default profile group?
             mailing_address : addr,
             addresses : [addr]
@@ -356,13 +360,13 @@ angular.module('egCoreMod')
         patron.cards([]);
         angular.forEach(card_hashes, function(chash) {
             var card = new egCore.idl.ac();
-            patron.cards().push(card);
             for (var key in chash) {
                 if (typeof card[key] == 'function') 
                     card[key](chash[key]);
             }
             card.usr(patron.id());
             card.active(chash.active ? 't' : 'f');
+            patron.cards().push(card);
 
             if (chash._primary) {
                 patron.card(card);
@@ -385,6 +389,8 @@ angular.module('egCoreMod')
             if (addr_hash._is_billing) patron.billing_address(addr);
         });
 
+        // TODO extract hold_notify_phone, etc.
+
         egCore.net.request(
             'open-ils.actor', 
             'open-ils.actor.patron.update',
@@ -447,6 +453,26 @@ function PatronRegCtrl($scope, $routeParams,
 
         if ($scope.org_settings['ui.patron.edit.default_suggested'])
             $scope.edit_passthru.vis_level = 1;
+
+        if ($scope.patron.isnew) {
+            $scope.generate_password();
+            $scope.hold_notify_phone = true;
+            $scope.hold_notify_email = true;
+
+            if (prs.org_settings['ui.patron.default_ident_type']) {
+                $scope.patron.ident_type = 
+                    prs.org_settings['ui.patron.default_ident_type'];
+            }
+            if (prs.org_settings['ui.patron.default_inet_access_level']) {
+                $scope.patron.ident_type = 
+                    prs.org_settings['ui.patron.default_inet_access_level'];
+            }
+            if (prs.org_settings['ui.patron.default_country']) {
+                $scope.patron.addresses[0].country = 
+                    prs.org_settings['ui.patron.default_country'];
+            }
+        }
+            
     });
 
     // returns the tree depth of the selected profile group tree node.
@@ -583,11 +609,12 @@ function PatronRegCtrl($scope, $routeParams,
     $scope.replace_card = function() {
         $scope.patron.card.active = false;
         $scope.patron.card.ischanged = true;
+
         var new_card = egCore.idl.toHash(new egCore.idl.ac());
         new_card.id = new_card_id--;
         new_card.isnew = true;
         new_card.active = true;
-        new_card._primary = true;
+        new_card._primary = 'on';
         $scope.patron.card = new_card;
         $scope.patron.cards.push(new_card);
     }

commit c273f651551d5b84a3efebb7978601295e2bec84
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Sep 16 21:42:16 2015 -0400

    LP#1452950 patron reg replace barcode
    
    Replace patron barcode.  Includes duplicate barcode detection, but no
    styling/warning is produced when a dupe is found, since the structure
    for handling invalid form fields in patron reg does not yet exist.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index f9abc18..d70c60f 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -64,6 +64,9 @@
     [% ELSIF field == 'post_code' %]
       <input type="text" ng-blur="post_code_changed(patron.[% path %])"
         class="form-control" ng-model="[% model %]"/>
+    [% ELSIF field == 'barcode' %]
+      <input type="text" ng-blur="barcode_changed(patron.card.barcode)"
+        class="form-control" ng-model="[% model %]"/>
     [% ELSE %]
       <input type="[% input_type %]" 
         class="form-control" ng-model="[% model %]"/>
@@ -77,7 +80,8 @@
 
     [% IF field == 'barcode' %]
 
-      <button class="btn btn-default">[% l('Replace Barcode') %]</button>
+      <button class="btn btn-default"
+        ng-click="replace_card()">[% l('Replace Barcode') %]</button>
       <button class="btn btn-default" 
         ng-click="cards_dialog()">[% l('See All') %]</button>
 
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 c88dbee..d9cd490 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
@@ -404,7 +404,6 @@ angular.module('egCoreMod')
 function PatronRegCtrl($scope, $routeParams, 
     $q, $modal, egCore, patronSvc, patronRegSvc) {
 
-
     $scope.clone_id = $routeParams.clone_id;
     $scope.stage_username = $routeParams.stage_username;
     $scope.patron_id = 
@@ -580,6 +579,35 @@ function PatronRegCtrl($scope, $routeParams,
         });
     }
 
+    var new_card_id = -1;
+    $scope.replace_card = function() {
+        $scope.patron.card.active = false;
+        $scope.patron.card.ischanged = true;
+        var new_card = egCore.idl.toHash(new egCore.idl.ac());
+        new_card.id = new_card_id--;
+        new_card.isnew = true;
+        new_card.active = true;
+        new_card._primary = true;
+        $scope.patron.card = new_card;
+        $scope.patron.cards.push(new_card);
+    }
+
+    $scope.barcode_changed = function(bc) {
+        if (!bc) return;
+        egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.barcode.exists',
+            egCore.auth.token(), bc
+        ).then(function(resp) {
+            if (resp == '1') {
+                console.log('duplicate barcode detected: ' + bc);
+                // DUPLICATE CARD
+            } else {
+                // No dupe -- A-OK
+            }
+        });
+    }
+
     $scope.cards_dialog = function() {
         $modal.open({
             templateUrl: './circ/patron/t_patron_cards_dialog',

commit 340dd74e9f387207331cfc7c5941d76af4816f1d
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Sep 13 22:03:47 2015 -0400

    LP#1452950 patron reg initial save operation
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2 b/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
index 12aa57f..a35b949 100644
--- a/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
@@ -23,7 +23,8 @@
 <div class="flex-cell"></div>
 <div>
   <span class="pad-all-min">
-    <button type="button" class="btn btn-default">[% l('Save') %]</button>
+    <button type="button" class="btn btn-default" 
+      ng-click="edit_passthru.save()">[% l('Save') %]</button>
   </span>
   <span class="pad-all-min">
     <button type="button" class="btn btn-default">[% l('Save & Clone') %]</button>
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 5fd2273..c88dbee 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
@@ -299,7 +299,7 @@ angular.module('egCoreMod')
         );
 
         angular.forEach(patron.cards, function(card) {
-            card.active = card.active == 't'
+            card.active = card.active == 't';
             if (card.id == patron.card.id)
                 card._primary = 'on';
         });
@@ -329,6 +329,74 @@ angular.module('egCoreMod')
         };
     }
 
+    // translate the patron back into IDL form
+    service.save_patron = function(phash) {
+
+        var patron = new egCore.idl.au();
+
+        for (var key in phash) {
+            if (typeof patron[key] == 'function')
+                patron[key](phash[key]);
+        }
+
+        patron.home_ou(patron.home_ou().id());
+        patron.expire_date(
+            patron.expire_date().toISOString().replace(/T.*/,''));
+        patron.dob(patron.dob().toISOString().replace(/T.*/,''));
+        patron.profile(patron.profile().id());
+        patron.net_access_level(patron.net_access_level().id());
+        patron.ident_type(patron.ident_type().id());
+
+        angular.forEach(
+            ['juvenile', 'barred', 'active', 'master_account'],
+            function(field) { patron[field](phash[field] ? 't' : 'f'); }
+        );
+
+        var card_hashes = patron.cards();
+        patron.cards([]);
+        angular.forEach(card_hashes, function(chash) {
+            var card = new egCore.idl.ac();
+            patron.cards().push(card);
+            for (var key in chash) {
+                if (typeof card[key] == 'function') 
+                    card[key](chash[key]);
+            }
+            card.usr(patron.id());
+            card.active(chash.active ? 't' : 'f');
+
+            if (chash._primary) {
+                patron.card(card);
+            }
+        });
+
+        var addr_hashes = patron.addresses();
+        patron.addresses([]);
+        angular.forEach(addr_hashes, function(addr_hash) {
+            var addr = new egCore.idl.aua();
+            patron.addresses().push(addr);
+            for (var key in addr_hash) {
+                if (typeof addr[key] == 'function') 
+                    addr[key](addr_hash[key]);
+            }
+
+            addr.valid(addr.valid() ? 't' : 'f');
+            addr.within_city_limits(addr.within_city_limits() ? 't' : 'f');
+            if (addr_hash._is_mailing) patron.mailing_address(addr);
+            if (addr_hash._is_billing) patron.billing_address(addr);
+        });
+
+        egCore.net.request(
+            'open-ils.actor', 
+            'open-ils.actor.patron.update',
+            egCore.auth.token(), patron)
+        .then(function(resp) {
+            // TODO: see original
+            console.log(js2JSON(resp));
+        });
+
+        console.log(js2JSON(patron));
+    }
+
     return service;
 }]);
 
@@ -542,6 +610,10 @@ function PatronRegCtrl($scope, $routeParams,
         );
     }
 
+    $scope.edit_passthru.save = function() {
+        patronRegSvc.save_patron($scope.patron);        
+    }
+
 }
 
 

commit 04499c1ef890839f306b9e1803b25992751cd5cc
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Sep 1 21:02:56 2015 -0400

    LP#1452950 patron reg barcodes dialog
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 2aa6d57..f9abc18 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -78,7 +78,8 @@
     [% IF field == 'barcode' %]
 
       <button class="btn btn-default">[% l('Replace Barcode') %]</button>
-      <button class="btn btn-default">[% l('See All') %]</button>
+      <button class="btn btn-default" 
+        ng-click="cards_dialog()">[% l('See All') %]</button>
 
     [% ELSIF field == 'passwd' %]
 
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_patron_cards_dialog.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_patron_cards_dialog.tt2
new file mode 100644
index 0000000..4ef6b32
--- /dev/null
+++ b/Open-ILS/src/templates/staff/circ/patron/t_patron_cards_dialog.tt2
@@ -0,0 +1,34 @@
+<form ng-submit="ok(args)" role="form">
+    <div class="modal-header">
+      <button type="button" class="close" ng-click="cancel()" 
+        aria-hidden="true">×</button>
+      <h4 class="modal-title">[% l('Patron Barcodes') %]</h4>
+    </div>
+    <div class="modal-body patron-reg-barcodes">
+      <div class="row header">
+        <div class="col-md-4">
+          <label>[% l('Barcode') %]</label>
+        </div>
+        <div class="col-md-4">
+          <label>[% l('Active') %]</label>
+        </div>
+        <div class="col-md-4">
+          <label>[% l('Primary') %]</label>
+        </div>
+      </div>
+      <div class="row" ng-repeat="card in args.cards">
+        <div class="col-md-4">{{card.barcode}}</div>
+        <div class="col-md-4">
+          <input type='checkbox' ng-model='card.active'/>
+        </div>
+        <div class="col-md-4">
+          <input type='radio' name='primary' value='on' ng-model='card._primary'/>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <input type="submit" class="btn btn-primary" value="[% l('Apply Changes') %]"/>
+      <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+    </div>
+  </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/css/circ.css.tt2 b/Open-ILS/src/templates/staff/css/circ.css.tt2
index 05cea1b..efc04f7 100644
--- a/Open-ILS/src/templates/staff/css/circ.css.tt2
+++ b/Open-ILS/src/templates/staff/css/circ.css.tt2
@@ -141,6 +141,10 @@ but the ones I'm finding aren't quite cutting it..*/
   font-weight: bold;
 }
 
+.patron-reg-barcodes > .header {
+  font-weight: bold;
+}
+
 /* -- end patron registration -- */
 
 [%# 
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 09a4e57..5fd2273 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
@@ -298,6 +298,12 @@ angular.module('egCoreMod')
             function(field) { patron[field] = patron[field] == 't'; }
         );
 
+        angular.forEach(patron.cards, function(card) {
+            card.active = card.active == 't'
+            if (card.id == patron.card.id)
+                card._primary = 'on';
+        });
+
         angular.forEach(patron.addresses, 
             function(addr) { service.ingest_address(patron, addr) });
 
@@ -328,7 +334,7 @@ angular.module('egCoreMod')
 
 
 function PatronRegCtrl($scope, $routeParams, 
-    $q, egCore, patronSvc, patronRegSvc) {
+    $q, $modal, egCore, patronSvc, patronRegSvc) {
 
 
     $scope.clone_id = $routeParams.clone_id;
@@ -495,7 +501,6 @@ function PatronRegCtrl($scope, $routeParams,
     } 
 
     $scope.post_code_changed = function(addr) { 
-        console.log('post code ' + addr.post_code);
         egCore.net.request(
             'open-ils.search', 'open-ils.search.zip', addr.post_code)
         .then(function(resp) {
@@ -506,6 +511,37 @@ function PatronRegCtrl($scope, $routeParams,
             if (resp.alert) alert(resp.alert);
         });
     }
+
+    $scope.cards_dialog = function() {
+        $modal.open({
+            templateUrl: './circ/patron/t_patron_cards_dialog',
+            controller: 
+                   ['$scope','$modalInstance','cards',
+            function($scope , $modalInstance , cards) {
+                // scope here is the modal-level scope
+                $scope.args = {cards : cards};
+                $scope.ok = function() { $modalInstance.close($scope.args) }
+                $scope.cancel = function () { $modalInstance.dismiss() }
+            }],
+            resolve : {
+                cards : function() {
+                    // scope here is the controller-level scope
+                    return $scope.patron.cards;
+                }
+            }
+        }).result.then(
+            function(args) {
+                angular.forEach(args.cards, function(card) {
+                    card.ischanged = true; // assume cards need updating, OK?
+                    if (card._primary == 'on' && 
+                        card.id != $scope.patron.card.id) {
+                        $scope.patron.card = card;
+                    }
+                });
+            }
+        );
+    }
+
 }
 
 

commit 04606618b40ef10d95e15393c17c4added059579
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Aug 30 21:34:52 2015 -0400

    LP#1452950 patron reg post code lookup
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 4bf4792..2aa6d57 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -61,6 +61,9 @@
 
     [% IF field == 'alert_message' %]
       <textarea class="form-control" ng-model="[% model %]"/>
+    [% ELSIF field == 'post_code' %]
+      <input type="text" ng-blur="post_code_changed(patron.[% path %])"
+        class="form-control" ng-model="[% model %]"/>
     [% ELSE %]
       <input type="[% input_type %]" 
         class="form-control" ng-model="[% model %]"/>
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 b676963..09a4e57 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
@@ -492,6 +492,19 @@ function PatronRegCtrl($scope, $routeParams,
             }
         });
         $scope.patron.addresses = addresses;
+    } 
+
+    $scope.post_code_changed = function(addr) { 
+        console.log('post code ' + addr.post_code);
+        egCore.net.request(
+            'open-ils.search', 'open-ils.search.zip', addr.post_code)
+        .then(function(resp) {
+            if (!resp) return;
+            if (resp.city) addr.city = resp.city;
+            if (resp.state) addr.state = resp.state;
+            if (resp.county) addr.county = resp.county;
+            if (resp.alert) alert(resp.alert);
+        });
     }
 }
 

commit 6882b669c93732bf791320d608ea8081002490c0
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Aug 30 21:10:23 2015 -0400

    LP#1452950 add/delete patron address
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 9912d59..4bf4792 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -352,39 +352,46 @@
   </div>
 </div>
 
-<div class="alert alert-success row" role="alert">
-    <div class="col-md-3">[% l('Address') %]</div>
-    <div class="col-md-3">
-        <span class='pad-all-min'>
-          [% l('Mailing') %] <input type='checkbox'/>
-        </span>
-        <span class='pad-all-min'>
-          [% l('Physical') %] <input type='checkbox'/>
-        </span>
-        <span class='pad-all-min'>
-          <button type="button" class="btn btn-danger">[% l('X') %]</button>
-        </span>
-    </div>
-</div>
 
 <!-- addresses -->
 
-[% formfield('aua', 'address_type', 'mailing_address') %]
-[% formfield('aua', 'post_code', 'mailing_address') %]
-[% formfield('aua', 'street1', 'mailing_address') %]
-[% formfield('aua', 'street2', 'mailing_address') %]
-[% formfield('aua', 'city', 'mailing_address') %]
-[% formfield('aua', 'county', 'mailing_address') %]
-[% formfield('aua', 'state', 'mailing_address') %]
-[% formfield('aua', 'country', 'mailing_address') %]
-[% formfield('aua', 'valid', 'mailing_address', 'checkbox') %]
-[% formfield('aua', 'within_city_limits', 'mailing_address', 'checkbox') %]
-
-<div class="row">
-  <button type="button" class="btn btn-success">[% l('New Address') %]</button>
-</div>
 
-<!-- pending address -->
+<div ng-repeat="addr in patron.addresses">
+  <div class="alert alert-success row" role="alert">
+      <div class="col-md-3">[% l('Address') %]</div>
+      <div class="col-md-3">
+          <span class='pad-all-min'>
+            [% l('Mailing') %] <input type='checkbox' ng-model="addr._is_mailing"/>
+          </span>
+          <span class='pad-all-min'>
+            [% l('Physical') %] <input type='checkbox' ng-model="addr._is_billing"/>
+          </span>
+          <span class='pad-all-min'>
+            <button type="button" ng-click="delete_address(addr.id)" 
+              class="btn btn-danger">[% l('X') %]</button>
+          </span>
+      </div>
+  </div>
+
+  [% formfield('aua', 'address_type', 'addresses[$index]') %]
+  [% formfield('aua', 'post_code', 'addresses[$index]') %]
+  [% formfield('aua', 'street1', 'addresses[$index]') %]
+  [% formfield('aua', 'street2', 'addresses[$index]') %]
+  [% formfield('aua', 'city', 'addresses[$index]') %]
+  [% formfield('aua', 'county', 'addresses[$index]') %]
+  [% formfield('aua', 'state', 'addresses[$index]') %]
+  [% formfield('aua', 'country', 'addresses[$index]') %]
+  [% formfield('aua', 'valid', 'addresses[$index]', 'checkbox') %]
+  [% formfield('aua', 'within_city_limits', 'addresses[$index]', 'checkbox') %]
+
+  <div class="row" ng-if="$last">
+    <button type="button" ng-click="new_address()" 
+      class="btn btn-success">[% l('New Address') %]</button>
+  </div>
+
+  <!-- pending address -->
+
+</div> <!-- addresses -->
 
 <div class="alert alert-success row" role="alert" 
     ng-show="show_field('stat_cats')" ng-if="stat_cats.length > 0">
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 3a46e61..b676963 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
@@ -265,6 +265,15 @@ angular.module('egCoreMod')
         return service.init_existing_patron(current)
     }
 
+    service.ingest_address = function(patron, addr) {
+        addr.valid = addr.valid == 't';
+        addr.within_city_limits = addr.within_city_limits == 't';
+        addr._is_mailing = (patron.mailing_address && 
+            addr.id == patron.mailing_address.id);
+        addr._is_billing = (patron.billing_address && 
+            addr.id == patron.billing_address.id);
+    }
+
     /*
      * Existing patron objects reqire some data munging before insertion
      * into the scope.
@@ -289,10 +298,8 @@ angular.module('egCoreMod')
             function(field) { patron[field] = patron[field] == 't'; }
         );
 
-        angular.forEach(patron.addresses, function(addr) {
-            addr.valid = addr.valid == 't';
-            addr.within_city_limits = addr.within_city_limits == 't';
-        });
+        angular.forEach(patron.addresses, 
+            function(addr) { service.ingest_address(patron, addr) });
 
         return patron;
     }
@@ -457,6 +464,35 @@ function PatronRegCtrl($scope, $routeParams,
         $scope.patron.profile = grp;
         $scope.set_expire_date();
     }
+
+    var new_addr_id = -1;
+    $scope.new_address = function() {
+        var addr = egCore.idl.toHash(new egCore.idl.aua());
+        patronRegSvc.ingest_address($scope.patron, addr);
+        addr.id = new_addr_id--;
+        addr.valid = true;
+        addr.within_city_limits = true;
+        $scope.patron.addresses.push(addr);
+    }
+
+    // keep deleted addresses out of the patron object so
+    // they won't appear in the UI.  They'll be re-inserted
+    // when the patron is updated.
+    deleted_addresses = [];
+    $scope.delete_address = function(id) {
+        var addresses = [];
+        angular.forEach($scope.patron.addresses, function(addr) {
+            if (addr.id == id) {
+                if (id > 0) {
+                    addr.isdeleted = true;
+                    deleted_addresses.push(addr);
+                }
+            } else {
+                addresses.push(addr);
+            }
+        });
+        $scope.patron.addresses = addresses;
+    }
 }
 
 

commit d61dcd4c8018b3784cfe9d1a791f9e6be841b168
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Aug 19 23:16:46 2015 -0400

    LP#1452950 browser client patron reg additions
    
    * generate password
    * update expire date
    * phone / email invalide buttons (display only)
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/index.tt2 b/Open-ILS/src/templates/staff/circ/patron/index.tt2
index 9cfd554..df0cea5 100644
--- a/Open-ILS/src/templates/staff/circ/patron/index.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/index.tt2
@@ -10,6 +10,7 @@
 <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/services/eframe.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/date.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/billing.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/circ.js"></script>
 [% INCLUDE 'staff/circ/share/circ_strings.tt2' %]
diff --git a/Open-ILS/src/templates/staff/circ/patron/register.tt2 b/Open-ILS/src/templates/staff/circ/patron/register.tt2
index ddd9da2..96332d2 100644
--- a/Open-ILS/src/templates/staff/circ/patron/register.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/register.tt2
@@ -6,6 +6,7 @@
 
 [% 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/services/date.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/register.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/regctl.js"></script>
 <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/circ.css" />
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 29c15ec..9912d59 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -70,19 +70,27 @@
   </div>
 
   <!-- supplemental actions and example text -->
-  <div class="col-md-3 patron-reg-example">
+  <div class="col-md-6 patron-reg-example">
 
     [% IF field == 'barcode' %]
 
       <button class="btn btn-default">[% l('Replace Barcode') %]</button>
       <button class="btn btn-default">[% l('See All') %]</button>
 
-    [% ELSIF field == 'password' %]
+    [% ELSIF field == 'passwd' %]
 
-      <button class="btn btn-default">[% l('Generate Password') %]</button>
+      <button class="btn btn-default" ng-click="generate_password()">
+        [% l('Generate Password') %]</button>
 
     [% ELSE %]
 
+      <!-- invalidate buttons -->
+
+      [% IF field.match('phone') OR field.match('email') %]
+        <button ng-show="patron.[% field %]" class="btn btn-default"
+          ng-click="">[% l('Invalidate') %]</button>
+      [% END %]
+
       <!-- example strings -->
 
       [% set_str = "org_settings['ui.patron.edit." _ 
@@ -92,15 +100,14 @@
         [% l('Example: [_1]', "{{" _ set_str _ "}}") %]
       </span>
 
+      <!-- phones have a fall-through example strings -->
       [% IF field.match('phone') %]
-        <!-- phones have a fall-through example option -->
         <span ng-if="![% set_str %] && org_settings['ui.patron.edit.phone.example']">
           [% l('Example: [_1]', 
-           "{{org_settings['ui.patron.edit.phone.example']}}") %]
+          "{{org_settings['ui.patron.edit.phone.example']}}") %]
         </span>
       [% END %]
     [% END %]
-
   </div>
 </div>
 [% END %]
@@ -200,11 +207,14 @@
         <li ng-repeat="grp in profiles">
           <a href 
             style="padding-left: {{pgt_depth(grp) * 10 + 5}}px"
-            ng-click="patron.profile = grp">{{grp.name()}}</a>
+            ng-click="set_profile(grp)">{{grp.name()}}</a>
         </li>
       </ul>
     </div>
   </div>
+  <div class="col-md-3">
+    <button class="btn btn-default">[% l('Secondary Groups') %]</button>
+  </div> 
 </div>
 
 <div class="row reg-field-row" ng-show="show_field('au.expire_date')">
@@ -219,7 +229,8 @@
       class="form-control" ng-model="patron.expire_date"/>
   </div>
   <div class="col-md-3">
-    <button class="btn btn-default">[% l('Update Expire Date') %]</button>
+    <button class="btn btn-default" ng-click="set_expire_date()">
+      [% l('Update Expire Date') %]</button>
   </div>
 </div>
 
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 f8aef69..3a46e61 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
@@ -437,6 +437,26 @@ function PatronRegCtrl($scope, $routeParams,
 
         return field_visibility[field_key] >= $scope.edit_passthru.vis_level;
     }
+
+    // generates a random 4-digit password
+    $scope.generate_password = function() {
+        $scope.patron.passwd = Math.floor(Math.random()*9000) + 1000;
+    }
+
+    $scope.set_expire_date = function() {
+        if (!$scope.patron.profile) return;
+        var seconds = egCore.date.intervalToSeconds(
+            $scope.patron.profile.perm_interval());
+        var now_epoch = new Date().getTime();
+        $scope.patron.expire_date = new Date(
+            now_epoch + (seconds * 1000 /* milliseconds */))
+    }
+
+    // grp is the pgt object
+    $scope.set_profile = function(grp) {
+        $scope.patron.profile = grp;
+        $scope.set_expire_date();
+    }
 }
 
 

commit 81248da60ffd29993a9352c9bc8ee5c1bea488d4
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Aug 19 22:21:29 2015 -0400

    LP#1452950 quiet chatty grid field logging
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.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 d02841e..4a5ab70 100644
--- a/Open-ILS/web/js/ui/default/staff/services/grid.js
+++ b/Open-ILS/web/js/ui/default/staff/services/grid.js
@@ -1261,7 +1261,9 @@ angular.module('egGridMod',
 
                     var col = cols.cloneFromScope(colSpec);
                     col.path = (dotpath ? dotpath + '.' + field.name : field.name);
-                    console.debug('egGrid: field: ' +field.name + '; parent field: ' + js2JSON(idl_field));
+
+                    // log line below is very chatty.  disable until needed.
+                    // console.debug('egGrid: field: ' +field.name + '; parent field: ' + js2JSON(idl_field));
                     cols.add(col, false, true, 
                         {idl_parent : idl_field, idl_field : field, idl_class : class_obj});
                 });

commit 1ef95797c8ad96456a715b12f82b73c9d94982fc
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Aug 18 09:47:20 2015 -0400

    LP#1452950 patron summary show/hide repair
    
    Recover the patron summary show/hide link, which was lost in the
    fixed-position elements shuffle.  This moves the patron's name back into
    the fixed bar along the top so that it's always visible, as before.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/index.tt2 b/Open-ILS/src/templates/staff/circ/patron/index.tt2
index 1db25c2..9cfd554 100644
--- a/Open-ILS/src/templates/staff/circ/patron/index.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/index.tt2
@@ -54,15 +54,37 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
   #top-content-container { padding-top: 80px; }
 </style>
 
-
 [% END %]
 
-<div class="row">
-  <div class="col-md-3">
-    <!-- just here to keep this slot open for the patron summary column -->
+<div class="row" id="patron-fixed-tabs">
+  <div class="col-md-3 patron-name-header">
+    <div ng-show="patron()">
+      <h4 title="{{patron().id()}}">
+        <div class="flex-row">
+          <div class="flex-cell">
+            [% l('[_1], [_2] [_3]', 
+                '{{patron().family_name()}}',
+                '{{patron().first_given_name()}}',
+                '{{patron().second_given_name()}}') %]
+          </div>
+          <div ng-show="tab != 'search'">
+            <a href ng-click="toggle_expand_summary()"
+              title="[% l('Collapse Patron Summary Display') %]"
+              ng-hide="collapse_summary()">
+              <span class="glyphicon glyphicon-resize-small"></span>
+            </a>
+            <a href ng-click="toggle_expand_summary()"
+              title="[% l('Expand Patron Summary Display') %]"
+              ng-show="collapse_summary()">
+              <span class="glyphicon glyphicon-resize-full"></span>
+            </a>
+          </div>
+        </div>
+      </h4>
+    </div>
   </div>
 
-  <div class="col-md-9" id="patron-fixed-tabs">
+  <div class="col-md-9">
     <ul class="nav nav-pills nav-pills-like-tabs">
       <li ng-class="{active : tab == 'checkout', disabled : !patron()}">
         <a a-disabled="!patron()" href="./circ/patron/{{patron().id()}}/checkout">[% l('Check Out') %]</a>
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_summary.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_summary.tt2
index 1cb43fa..40896dc 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_summary.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_summary.tt2
@@ -1,31 +1,5 @@
 
 <div ng-cloak class="patron-summary-grid-wrapper">
-  <div class="row">
-    <div class="col-md-12">
-      <h4 title="{{patron().id()}}">
-        <div class="flex-row">
-          <div class="flex-cell">
-            [% l('[_1], [_2] [_3]', 
-                '{{patron().family_name()}}',
-                '{{patron().first_given_name()}}',
-                '{{patron().second_given_name()}}') %]
-          </div>
-          <div ng-show="tab != 'search'">
-            <a href ng-click="toggle_expand_summary()"
-              title="[% l('Collapse Patron Summary Display') %]"
-              ng-hide="collapse_summary()">
-              <span class="glyphicon glyphicon-resize-small"></span>
-            </a>
-            <a href ng-click="toggle_expand_summary()"
-              title="[% l('Expand Patron Summary Display') %]"
-              ng-show="collapse_summary()">
-              <span class="glyphicon glyphicon-resize-full"></span>
-            </a>
-          </div>
-        </div>
-      </h4>
-    </div>
-  </div>
   <div ng-show="patron()" id="patron-summary-grid">
     <div class="row" 
       ng-class="{'patron-summary-divider' : !$index}"
diff --git a/Open-ILS/src/templates/staff/css/circ.css.tt2 b/Open-ILS/src/templates/staff/css/circ.css.tt2
index 551883e..05cea1b 100644
--- a/Open-ILS/src/templates/staff/css/circ.css.tt2
+++ b/Open-ILS/src/templates/staff/css/circ.css.tt2
@@ -2,7 +2,7 @@
 /* push the patron summary up to compensate for the extra
  * padding required to support the fixed navigation */
 .patron-summary-grid-wrapper {
-  margin-top: -60px;
+  margin-top: 20px;
 }
 
 /** style to make a grid look like a striped table */
@@ -29,12 +29,17 @@ but the ones I'm finding aren't quite cutting it..*/
     position: fixed;
     top: 50px;
     right: 15px;
+    left: 45px;
     padding-top: 20px;
     padding-top: 10px;
     z-index: 1;
     background-color: rgba(255,255,255,1);
 }
 
+.patron-name-header {
+  margin-top: 20px;
+}
+
 /* let search form elements fill their containers w/ slight padding */
 #patron-search-form-row {margin-left: 0px;}
 #patron-search-form div.col-md-2 { padding: 2px; }

commit ced2598e8f7957591a8573b11b21a46aaf31e377
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sat Aug 15 15:41:59 2015 -0400

    LP#1452950 required/suggested/all fields selectors
    
    Wire up links for Required, suggested, and All fields links.
    
    Also move the patron control bar out to its own template since it must
    be loaded from 2 different places in the markup, one for edit and one
    for register.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/index.tt2 b/Open-ILS/src/templates/staff/circ/patron/index.tt2
index a224fa1..1db25c2 100644
--- a/Open-ILS/src/templates/staff/circ/patron/index.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/index.tt2
@@ -145,29 +145,7 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
       </li>
     </ul>
     <div class="flex-row patron-reg-actions-bar" ng-if="is_patron_edit()">
-      <div>
-        <span>
-          [% l('Show:') %]
-          <span class="pad-all-min" ng-show="!display_required_fields">
-            <a href>[% l('Required Fields') %]</a>
-          </span>
-          <span class="pad-all-min" ng-show="!display_suggested_fields">
-            <a href>[% l('Suggested Fields') %]</a>
-          </span>
-          <span class="pad-all-min" ng-show="!display_all_fields">
-            <a href>[% l('All Fields') %]</a>
-          </span>
-        </span>
-      </div>
-      <div class="flex-cell"></div>
-      <div>
-        <span class="pad-all-min">
-          <button type="button" class="btn btn-default">[% l('Save') %]</button>
-        </span>
-        <span class="pad-all-min">
-          <button type="button" class="btn btn-default">[% l('Save & Clone') %]</button>
-        </span>
-      </div>
+      [% INCLUDE 'staff/circ/patron/reg_actions.tt2' %]
     </div>
 
   </div><!-- col -->
diff --git a/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2 b/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
new file mode 100644
index 0000000..12aa57f
--- /dev/null
+++ b/Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
@@ -0,0 +1,32 @@
+<!-- actions bar shared by both variations of the patron edit UI -->
+
+<div>
+  <span>
+    [% l('Show:') %]
+    <span class="pad-all-min">
+      <a href 
+        ng-class="{disabled : edit_passthru.vis_level == 2}"
+        ng-click="edit_passthru.vis_level=2">[% l('Required Fields') %]</a>
+    </span>
+    <span class="pad-all-min">
+      <a href 
+        ng-class="{disabled : edit_passthru.vis_level == 1}"
+        ng-click="edit_passthru.vis_level=1">[% l('Suggested Fields') %]</a>
+    </span>
+    <span class="pad-all-min">
+      <a href 
+        ng-class="{disabled : edit_passthru.vis_level == 0}"
+        ng-click="edit_passthru.vis_level=0">[% l('All Fields') %]</a>
+    </span>
+  </span>
+</div>
+<div class="flex-cell"></div>
+<div>
+  <span class="pad-all-min">
+    <button type="button" class="btn btn-default">[% l('Save') %]</button>
+  </span>
+  <span class="pad-all-min">
+    <button type="button" class="btn btn-default">[% l('Save & Clone') %]</button>
+  </span>
+</div>
+
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 8d82201..29c15ec 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -10,32 +10,11 @@
   </div>
 
   <div class="flex-row" class='patron-reg-actions-bar'>
-    <div>
-      <span>
-        [% l('Show:') %]
-        <span class="pad-all-min" ng-show="!display_required_fields">
-          <a href>[% l('Required Fields') %]</a>
-        </span>
-        <span class="pad-all-min" ng-show="!display_suggested_fields">
-          <a href>[% l('Suggested Fields') %]</a>
-        </span>
-        <span class="pad-all-min" ng-show="!display_all_fields">
-          <a href>[% l('All Fields') %]</a>
-        </span>
-      </span>
-    </div>
-    <div class="flex-cell"></div>
-    <div>
-      <span class="pad-all-min">
-        <button type="button" class="btn btn-default">[% l('Save') %]</button>
-      </span>
-      <span class="pad-all-min">
-        <button type="button" class="btn btn-default">[% l('Save & Clone') %]</button>
-      </span>
-    </div>
+    [% INCLUDE 'staff/circ/patron/reg_actions.tt2' %]
   </div>
 </div>
 
+
 <!-- edit banner -->
 <div ng-if="patron_id"
     class="strong-text-2">[% l('Patron Edit') %]</div>
@@ -54,16 +33,17 @@
 
   IF NOT input_type; input_type = 'text'; END %] 
 
-<div class="row reg-field-row">
+<div class="row reg-field-row" 
+  ng-show="show_field('[% cls _ '.' _ field %]')">
 
   <div class="col-md-3 reg-field-label"> <!-- field label -->
 
-  <label>{{idl_fields.[% cls %].[% field %].label}}</label>
+    <label>{{idl_fields.[% cls %].[% field %].label}}</label>
 
-  <!-- field documentation img/link -->
-  <img ng-show="field_doc.[% cls %].[% field %]" 
-    ng-click="selected_field_doc=field_doc.[% cls %].[% field %]"
-    src='[% DOC_IMG %]'></img>
+    <!-- field documentation img/link -->
+    <img ng-show="field_doc.[% cls %].[% field %]" 
+      ng-click="selected_field_doc=field_doc.[% cls %].[% field %]"
+      src='[% DOC_IMG %]'></img>
   </div>
 
   <div class="col-md-3 reg-field-input"> <!-- field form input -->
@@ -135,7 +115,7 @@
 [% formfield('au', 'suffix') %]
 [% formfield('au', 'alias') %]
 
-<div class="row reg-field-row">
+<div class="row reg-field-row" ng-show="show_field('au.dob')">
   <div class="col-md-3 reg-field-label">
     <label>{{idl_fields.au.dob.label}}</label>
     <img ng-show="field_doc.au.dob" 
@@ -152,7 +132,7 @@
 
 <!-- ident_type -->
 
-<div class="row reg-field-row">
+<div class="row reg-field-row" ng-show="show_field('au.ident_type')">
   <div class="col-md-3 reg-field-label">
     <label>{{idl_fields.au.ident_type.label}}</label>
     <img ng-show="field_doc.au.ident_type" 
@@ -186,7 +166,7 @@
 
 <!-- home org unit selector -->
 
-<div class="row reg-field-row">
+<div class="row reg-field-row" ng-show="show_field('au.home_ou')">
   <div class="col-md-3 reg-field-label">
     <label>{{idl_fields.au.home_ou.label}}</label>
     <img ng-show="field_doc.au.home_ou" 
@@ -201,7 +181,7 @@
 
 <!-- profile selector -->
 
-<div class="row reg-field-row">
+<div class="row reg-field-row" ng-show="show_field('au.profile')">
   <div class="col-md-3 reg-field-label">
     <label>{{idl_fields.au.profile.label}}</label>
     <img ng-show="field_doc.au.profile" 
@@ -227,7 +207,7 @@
   </div>
 </div>
 
-<div class="row reg-field-row">
+<div class="row reg-field-row" ng-show="show_field('au.expire_date')">
   <div class="col-md-3 reg-field-label">
   <label>{{idl_fields.au.expire_date.label}}</label>
     <img ng-show="field_doc.au.expire_date" 
@@ -245,7 +225,7 @@
 
 <!-- net_access_level -->
 
-<div class="row reg-field-row">
+<div class="row reg-field-row" ng-show="show_field('au.net_access_level')">
   <div class="col-md-3 reg-field-label">
     <label>{{idl_fields.au.net_access_level.label}}</label>
     <img ng-show="field_doc.au.net_access_level" 
@@ -395,11 +375,13 @@
 
 <!-- pending address -->
 
-<div class="alert alert-success row" role="alert" ng-if="stat_cats.length > 0">
+<div class="alert alert-success row" role="alert" 
+    ng-show="show_field('stat_cats')" ng-if="stat_cats.length > 0">
     <div class="col-md-6">[% l('Statistical Categories') %]</div>
 </div>
 
-<div class="row reg-field-row" ng-repeat="cat in stat_cats">
+<div class="row reg-field-row" 
+    ng-show="show_field('stat_cats')" ng-repeat="cat in stat_cats">
   <div class="col-md-3 reg-field-label">
     <label>{{cat.name()}}</label>
   </div>
@@ -425,11 +407,13 @@
 
 <!-- surveys -->
 
-<div class="alert alert-success row" role="alert" ng-if="surveys.length > 0">
+<div class="alert alert-success row" role="alert" 
+    ng-show="show_field('surveys')" ng-if="surveys.length > 0">
     <div class="col-md-6">[% l('Surveys') %]</div>
 </div>
 
-<div class="row reg-field-row" ng-repeat="survey in surveys">
+<div class="row reg-field-row" 
+    ng-show="show_field('surveys')" ng-repeat="survey in surveys">
   <div class="col-md-3 reg-field-label">
     <label>{{survey.name()}}</label>
   </div>
diff --git a/Open-ILS/src/templates/staff/css/circ.css.tt2 b/Open-ILS/src/templates/staff/css/circ.css.tt2
index b2049ad..551883e 100644
--- a/Open-ILS/src/templates/staff/css/circ.css.tt2
+++ b/Open-ILS/src/templates/staff/css/circ.css.tt2
@@ -136,7 +136,6 @@ but the ones I'm finding aren't quite cutting it..*/
   font-weight: bold;
 }
 
-
 /* -- end patron registration -- */
 
 [%# 
diff --git a/Open-ILS/src/templates/staff/css/style.css.tt2 b/Open-ILS/src/templates/staff/css/style.css.tt2
index 5c367b8..c2ff924 100644
--- a/Open-ILS/src/templates/staff/css/style.css.tt2
+++ b/Open-ILS/src/templates/staff/css/style.css.tt2
@@ -102,6 +102,13 @@
  * Local style
  */
 
+/* no bootstrap way to directly disable a link.  */
+a.disabled {
+  pointer-events: none;
+  cursor: default;
+  color: #888;
+}
+
 #splash-nav .panel-body div {
     padding-bottom: 10px;
 }
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 9d98693..117d8f8 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
@@ -561,6 +561,12 @@ function($scope,  $q,  $location , $filter,  egCore,  egUser,  patronSvc) {
         return Boolean($location.path().match(/patron\/\d+\/edit$/));
     }
 
+    // To support the fixed position patron edit actions bar,
+    // its markup has to live outside the scope of the patron 
+    // edit controller.  Insert a scope blob here that can be
+    // modifed from within the patron edit controller.
+    $scope.edit_passthru = {};
+
     // returns true if a redirect occurs
     function redirectToAlertPanel() {
 
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 b940514..f8aef69 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
@@ -134,20 +134,30 @@ angular.module('egCoreMod')
     // some org settings require the retrieval of additional data
     service.process_org_settings = function(settings) {
 
-        if (!settings['sms.enable']) {
-            return $q.when();
+        var promises = [];
+
+        if (settings['sms.enable']) {
+            // fetch SMS carriers
+            promises.push(
+                egCore.pcrud.search('csc', 
+                    {active: 'true'}, 
+                    {'order_by':[
+                        {'class':'csc', 'field':'name'},
+                        {'class':'csc', 'field':'region'}
+                    ]}, {atomic : true}
+                ).then(function(carriers) {
+                    service.sms_carriers = carriers;
+                })
+            );
+        } else {
+            // if other promises are added below, this is not necessary.
+            promises.push($q.when());  
         }
 
-        return egCore.pcrud.search('csc', 
-            {active: 'true'}, 
-            {'order_by':[
-                {'class':'csc', 'field':'name'},
-                {'class':'csc', 'field':'region'}
-            ]},
-            {atomic : true}
-        ).then(function(carriers) {
-            service.sms_carriers = carriers;
-        });
+        // other post-org-settings processing goes here,
+        // adding to promises as needed.
+
+        return $q.all(promises);
     };
 
     service.get_ident_types = function() {
@@ -313,11 +323,23 @@ angular.module('egCoreMod')
 function PatronRegCtrl($scope, $routeParams, 
     $q, egCore, patronSvc, patronRegSvc) {
 
+
     $scope.clone_id = $routeParams.clone_id;
     $scope.stage_username = $routeParams.stage_username;
     $scope.patron_id = 
         patronRegSvc.patron_id = $routeParams.edit_id || $routeParams.id;
 
+    if (!$scope.edit_passthru) {
+        // in edit more, scope.edit_passthru is delivered to us by
+        // the enclosing controller.  In register mode, there is 
+        // no enclosing controller, so we create our own.
+        $scope.edit_passthru = {};
+    }
+
+    // 0=all, 1=suggested, 2=all
+    $scope.edit_passthru.vis_level = 0; 
+    // TODO: add save/clone handlers here
+
     $q.all([
 
         $scope.initTab ? // initTab comes from patron app
@@ -342,6 +364,9 @@ function PatronRegCtrl($scope, $routeParams,
         $scope.sms_carriers = prs.sms_carriers;
         $scope.stat_cats = prs.stat_cats;
         $scope.surveys = prs.surveys;
+
+        if ($scope.org_settings['ui.patron.edit.default_suggested'])
+            $scope.edit_passthru.vis_level = 1;
     });
 
     // returns the tree depth of the selected profile group tree node.
@@ -358,6 +383,60 @@ function PatronRegCtrl($scope, $routeParams,
         aua : egCore.idl.classes.aua.field_map
     };
 
+    // field visibility cache.  Some fields are universally required.
+    var field_visibility = {
+        'ac.barcode' : 2,
+        'au.usrname' : 2,
+        'au.passwd' :  2,
+        // TODO passwd2 2,
+        'au.first_given_name' : 2,
+        'au.family_name' : 2,
+        'au.ident_type' : 2,
+        'au.home_ou' : 2,
+        'au.profile' : 2,
+        'au.expire_date' : 2,
+        'au.net_access_level' : 2,
+        'aua.address_type' : 2,
+        'aua.post_code' : 2,
+        'aua.street1' : 2,
+        'aua.street2' : 2,
+        'aua.city' : 2,
+        'aua.county' : 2,
+        'aua.state' : 2,
+        'aua.country' : 2,
+        'aua.valid' : 2,
+        'aua.within_city_limits' : 2,
+        'stat_cats' : 1,
+        'surveys' : 1
+    }; 
+
+    // returns true if the selected field should be visible
+    // given the current required/suggested/all setting.
+    $scope.show_field = function(field_key) {
+
+        if (field_visibility[field_key] == undefined) {
+            // compile and cache the visibility for the selected field
+
+            // org settings have not been received yet.
+            if (!$scope.org_settings) return false;
+
+            var req_set = 'ui.patron.edit.' + field_key + '.require';
+            var sho_set = 'ui.patron.edit.' + field_key + '.show';
+            var sug_set = 'ui.patron.edit.' + field_key + '.suggest';
+
+            if ($scope.org_settings[req_set]) {
+                field_visibility[field_key] = 2;
+            } else if ($scope.org_settings[sho_set]) {
+                field_visibility[field_key] = 2;
+            } else if ($scope.org_settings[sug_set]) {
+                field_visibility[field_key] = 1;
+            } else {
+                field_visibility[field_key] = 0;
+            }
+        }
+
+        return field_visibility[field_key] >= $scope.edit_passthru.vis_level;
+    }
 }
 
 

commit c1134c519e644fe6a0d8bba788f93d3cb79a7025
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Aug 9 16:53:02 2015 -0400

    LP#1452950 patron reg fixed css repair
    
    Use CSS-style comments within <style> blocks, not HTML comments, lest
    your CSS stops working.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/register.tt2 b/Open-ILS/src/templates/staff/circ/patron/register.tt2
index cdbc199..ddd9da2 100644
--- a/Open-ILS/src/templates/staff/circ/patron/register.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/register.tt2
@@ -10,7 +10,7 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/regctl.js"></script>
 <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/circ.css" />
 <style>
-  <!-- add room for the fixed navigation elements -->
+  /* add room for the fixed navigation elements */
   #top-content-container { padding-top: 170px; }
 </style>
 [% END %]

commit e8014dcef616738ac8bac221bc52cf0307b31153
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Jul 30 22:38:41 2015 -0400

    LP#1452950 Patron reg fixed navigation
    
    Make the patron navigation tabs fixed.  Adds a fixed patron
    edit/registration action bar.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/index.tt2 b/Open-ILS/src/templates/staff/circ/patron/index.tt2
index 3234b95..a224fa1 100644
--- a/Open-ILS/src/templates/staff/circ/patron/index.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/index.tt2
@@ -46,36 +46,23 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
 }]);
 </script>
 
+<!-- add room for the fixed navigation elements -->
+<style ng-if="is_patron_edit">
+  #top-content-container { padding-top: 120px; }
+</style>
+<style ng-if="!is_patron_edit">
+  #top-content-container { padding-top: 80px; }
+</style>
+
+
 [% END %]
 
 <div class="row">
   <div class="col-md-3">
-    <div ng-show="patron()">
-      <h4 title="{{patron().id()}}">
-        <div class="flex-row">
-          <div class="flex-cell">
-            [% l('[_1], [_2] [_3]', 
-                '{{patron().family_name()}}',
-                '{{patron().first_given_name()}}',
-                '{{patron().second_given_name()}}') %]
-          </div>
-          <div ng-show="tab != 'search'">
-            <a href ng-click="toggle_expand_summary()"
-              title="[% l('Collapse Patron Summary Display') %]"
-              ng-hide="collapse_summary()">
-              <span class="glyphicon glyphicon-resize-small"></span>
-            </a>
-            <a href ng-click="toggle_expand_summary()"
-              title="[% l('Expand Patron Summary Display') %]"
-              ng-show="collapse_summary()">
-              <span class="glyphicon glyphicon-resize-full"></span>
-            </a>
-          </div>
-        </div><!-- row -->
-      </h4>
-    </div><!-- if patron -->
-  </div><!-- col -->
-  <div class="col-md-9">
+    <!-- just here to keep this slot open for the patron summary column -->
+  </div>
+
+  <div class="col-md-9" id="patron-fixed-tabs">
     <ul class="nav nav-pills nav-pills-like-tabs">
       <li ng-class="{active : tab == 'checkout', disabled : !patron()}">
         <a a-disabled="!patron()" href="./circ/patron/{{patron().id()}}/checkout">[% l('Check Out') %]</a>
@@ -157,6 +144,32 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
         <a href="./circ/patron/search">[% l('Patron Search') %]</a>
       </li>
     </ul>
+    <div class="flex-row patron-reg-actions-bar" ng-if="is_patron_edit()">
+      <div>
+        <span>
+          [% l('Show:') %]
+          <span class="pad-all-min" ng-show="!display_required_fields">
+            <a href>[% l('Required Fields') %]</a>
+          </span>
+          <span class="pad-all-min" ng-show="!display_suggested_fields">
+            <a href>[% l('Suggested Fields') %]</a>
+          </span>
+          <span class="pad-all-min" ng-show="!display_all_fields">
+            <a href>[% l('All Fields') %]</a>
+          </span>
+        </span>
+      </div>
+      <div class="flex-cell"></div>
+      <div>
+        <span class="pad-all-min">
+          <button type="button" class="btn btn-default">[% l('Save') %]</button>
+        </span>
+        <span class="pad-all-min">
+          <button type="button" class="btn btn-default">[% l('Save & Clone') %]</button>
+        </span>
+      </div>
+    </div>
+
   </div><!-- col -->
 </div><!-- row -->
 
diff --git a/Open-ILS/src/templates/staff/circ/patron/register.tt2 b/Open-ILS/src/templates/staff/circ/patron/register.tt2
index a0b2af4..cdbc199 100644
--- a/Open-ILS/src/templates/staff/circ/patron/register.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/register.tt2
@@ -9,6 +9,10 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/register.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/regctl.js"></script>
 <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/circ.css" />
+<style>
+  <!-- add room for the fixed navigation elements -->
+  #top-content-container { padding-top: 170px; }
+</style>
 [% END %]
 
 <div ng-view></div>
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 59bf318..8d82201 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -1,41 +1,52 @@
 [% DOC_IMG = '/images/question-mark.png' %]
 
 <!-- register banner -->
-<div ng-if="!patron_id" 
-  class="container-fluid" style="text-align:center">
-  <div class="alert alert-info alert-less-pad strong-text-2">
-    <span >[% l('Register Patron') %]</span>
+<div ng-if="!patron_id" class='patron-reg-fixed-bar'>
+
+  <div class="container-fluid" style="text-align:center">
+    <div class="alert alert-info alert-less-pad strong-text-2">
+      <span >[% l('Register Patron') %]</span>
+    </div>
+  </div>
+
+  <div class="flex-row" class='patron-reg-actions-bar'>
+    <div>
+      <span>
+        [% l('Show:') %]
+        <span class="pad-all-min" ng-show="!display_required_fields">
+          <a href>[% l('Required Fields') %]</a>
+        </span>
+        <span class="pad-all-min" ng-show="!display_suggested_fields">
+          <a href>[% l('Suggested Fields') %]</a>
+        </span>
+        <span class="pad-all-min" ng-show="!display_all_fields">
+          <a href>[% l('All Fields') %]</a>
+        </span>
+      </span>
+    </div>
+    <div class="flex-cell"></div>
+    <div>
+      <span class="pad-all-min">
+        <button type="button" class="btn btn-default">[% l('Save') %]</button>
+      </span>
+      <span class="pad-all-min">
+        <button type="button" class="btn btn-default">[% l('Save & Clone') %]</button>
+      </span>
+    </div>
   </div>
 </div>
 
 <!-- edit banner -->
-<div ng-if="patron_id" 
+<div ng-if="patron_id"
     class="strong-text-2">[% l('Patron Edit') %]</div>
 
 <!-- IDL field documentation window -->
-<div id="reg-control-actions">
-  <div class="pad-all-min">
-    <button type="button" class="btn btn-default">[% l('Save') %]</button>
-  </div>
-  <div class="pad-all-min">
-    <button type="button" class="btn btn-default">[% l('Save & Clone') %]</button>
-  </div>
-  <div class="pad-all-min" ng-show="!display_required_fields">
-    <a href>[% l('Required Fields') %]</a>
-  </div>
-  <div class="pad-all-min" ng-show="!display_suggested_fields">
-    <a href>[% l('Suggested Fields') %]</a>
-  </div>
-  <div class="pad-all-min" ng-show="!display_all_fields">
-    <a href>[% l('All Fields') %]</a>
-  </div>
-  <fieldset id="reg-field-doc" ng-show="selected_field_doc">
-    <legend>
-    {{idl_fields[selected_field_doc.fm_class()][selected_field_doc.field()].label}}
-    </legend>
-    <div>{{selected_field_doc.string()}}</div>
-  </fieldset>
-</div>
+<fieldset id="reg-field-doc" ng-show="selected_field_doc">
+  <legend>
+  {{idl_fields[selected_field_doc.fm_class()][selected_field_doc.field()].label}}
+  </legend>
+  <div>{{selected_field_doc.string()}}</div>
+</fieldset>
 
 [% MACRO formfield(cls, field, path, input_type) BLOCK;
 
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_summary.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_summary.tt2
index 7c880b9..1cb43fa 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_summary.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_summary.tt2
@@ -1,5 +1,31 @@
 
-<div ng-cloak>
+<div ng-cloak class="patron-summary-grid-wrapper">
+  <div class="row">
+    <div class="col-md-12">
+      <h4 title="{{patron().id()}}">
+        <div class="flex-row">
+          <div class="flex-cell">
+            [% l('[_1], [_2] [_3]', 
+                '{{patron().family_name()}}',
+                '{{patron().first_given_name()}}',
+                '{{patron().second_given_name()}}') %]
+          </div>
+          <div ng-show="tab != 'search'">
+            <a href ng-click="toggle_expand_summary()"
+              title="[% l('Collapse Patron Summary Display') %]"
+              ng-hide="collapse_summary()">
+              <span class="glyphicon glyphicon-resize-small"></span>
+            </a>
+            <a href ng-click="toggle_expand_summary()"
+              title="[% l('Expand Patron Summary Display') %]"
+              ng-show="collapse_summary()">
+              <span class="glyphicon glyphicon-resize-full"></span>
+            </a>
+          </div>
+        </div>
+      </h4>
+    </div>
+  </div>
   <div ng-show="patron()" id="patron-summary-grid">
     <div class="row" 
       ng-class="{'patron-summary-divider' : !$index}"
diff --git a/Open-ILS/src/templates/staff/css/circ.css.tt2 b/Open-ILS/src/templates/staff/css/circ.css.tt2
index 87a670c..b2049ad 100644
--- a/Open-ILS/src/templates/staff/css/circ.css.tt2
+++ b/Open-ILS/src/templates/staff/css/circ.css.tt2
@@ -1,3 +1,10 @@
+
+/* push the patron summary up to compensate for the extra
+ * padding required to support the fixed navigation */
+.patron-summary-grid-wrapper {
+  margin-top: -60px;
+}
+
 /** style to make a grid look like a striped table */
 #patron-summary-grid div.row {padding: 3px; border-right: 2px solid rgb(248, 248, 248);}
 #patron-summary-grid div.row:nth-child(odd) {background-color: rgb(248, 248, 248);}
@@ -18,6 +25,16 @@ but the ones I'm finding aren't quite cutting it..*/
   margin-bottom: 5px;
 }
 
+#patron-fixed-tabs {
+    position: fixed;
+    top: 50px;
+    right: 15px;
+    padding-top: 20px;
+    padding-top: 10px;
+    z-index: 1;
+    background-color: rgba(255,255,255,1);
+}
+
 /* let search form elements fill their containers w/ slight padding */
 #patron-search-form-row {margin-left: 0px;}
 #patron-search-form div.col-md-2 { padding: 2px; }
@@ -77,19 +94,10 @@ but the ones I'm finding aren't quite cutting it..*/
 
 
 /* floating div along top-right with field documentation */
-#reg-control-actions {
+#reg-field-doc {
     position: fixed;
     top:160px;
-    right:10px;
-    /*width:200px;*/
-    border:2px dashed #d9e8f9;
-    -moz-border-radius: 10px;
-    font-weight: bold;
-    padding: 10px;
-    margin-top: 10px;
-}
-
-#reg-field-doc {
+    right:20px;
     border:2px dashed #d9e8f9;
     -moz-border-radius: 10px;
     font-weight: bold;
@@ -110,6 +118,25 @@ but the ones I'm finding aren't quite cutting it..*/
     font-weight: normal;
 }
 
+.patron-reg-fixed-bar {
+    position: fixed;
+    top:50px;
+    right: 20px;
+    left: 20px;
+    padding-top: 20px;
+    padding-bottom: 10px;
+    z-index: 1;
+    background-color: rgba(255,255,255,1);
+}
+
+.patron-reg-actions-bar {
+  padding: 5px;
+}
+.patron-reg-actions-bar span {
+  font-weight: bold;
+}
+
+
 /* -- end patron registration -- */
 
 [%# 
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 46aab7c..9d98693 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
@@ -557,6 +557,10 @@ function($q , $timeout , $location , egCore,  egUser , $locale) {
        ['$scope','$q','$location','$filter','egCore','egUser','patronSvc',
 function($scope,  $q,  $location , $filter,  egCore,  egUser,  patronSvc) {
 
+    $scope.is_patron_edit = function() {
+        return Boolean($location.path().match(/patron\/\d+\/edit$/));
+    }
+
     // returns true if a redirect occurs
     function redirectToAlertPanel() {
 

commit 2279864bf129510c2502a8898a693fadc81ada2b
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun Jul 19 10:58:57 2015 -0400

    LP#1452950 Register v.s. Edit patron banners
    
    Register patron now has a page-level banner consistent w/ other
    full-page UI's.  Patron edit gets a smaller header since it's nestled
    under the patron tabs.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index b686c9a..59bf318 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -1,11 +1,17 @@
 [% DOC_IMG = '/images/question-mark.png' %]
 
-<div class="container-fluid" style="text-align:center">
+<!-- register banner -->
+<div ng-if="!patron_id" 
+  class="container-fluid" style="text-align:center">
   <div class="alert alert-info alert-less-pad strong-text-2">
-    <span ng-if="!is_capture">[% l('Patron Edit') %]</span>
+    <span >[% l('Register Patron') %]</span>
   </div>
 </div>
 
+<!-- edit banner -->
+<div ng-if="patron_id" 
+    class="strong-text-2">[% l('Patron Edit') %]</div>
+
 <!-- IDL field documentation window -->
 <div id="reg-control-actions">
   <div class="pad-all-min">

commit 89369b785a9933caf8128aacafb33670e6a7a249
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Jul 14 19:49:58 2015 -0400

    LP#1452950 Patrong reg. style repairs
    
    * Reduce vertical space by a few pixels
    * Make field labels non-bold
    * Make input fields bold
    * Remove duplicate padding to avoid label misalignment.
    * Use blue alert-info banner along the top of the screen.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index eef3cca..b686c9a 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -1,6 +1,10 @@
 [% DOC_IMG = '/images/question-mark.png' %]
 
-<div class="strong-text-2">[% l('Patron Edit') %]</div>
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    <span ng-if="!is_capture">[% l('Patron Edit') %]</span>
+  </div>
+</div>
 
 <!-- IDL field documentation window -->
 <div id="reg-control-actions">
@@ -33,7 +37,7 @@
 
   IF NOT input_type; input_type = 'text'; END %] 
 
-<div class="row form-group">
+<div class="row reg-field-row">
 
   <div class="col-md-3 reg-field-label"> <!-- field label -->
 
@@ -114,7 +118,7 @@
 [% formfield('au', 'suffix') %]
 [% formfield('au', 'alias') %]
 
-<div class="row pad-all-min form-group">
+<div class="row reg-field-row">
   <div class="col-md-3 reg-field-label">
     <label>{{idl_fields.au.dob.label}}</label>
     <img ng-show="field_doc.au.dob" 
@@ -131,7 +135,7 @@
 
 <!-- ident_type -->
 
-<div class="row pad-all-min form-group">
+<div class="row reg-field-row">
   <div class="col-md-3 reg-field-label">
     <label>{{idl_fields.au.ident_type.label}}</label>
     <img ng-show="field_doc.au.ident_type" 
@@ -165,7 +169,7 @@
 
 <!-- home org unit selector -->
 
-<div class="row pad-all-min form-group">
+<div class="row reg-field-row">
   <div class="col-md-3 reg-field-label">
     <label>{{idl_fields.au.home_ou.label}}</label>
     <img ng-show="field_doc.au.home_ou" 
@@ -180,7 +184,7 @@
 
 <!-- profile selector -->
 
-<div class="row pad-all-min form-group">
+<div class="row reg-field-row">
   <div class="col-md-3 reg-field-label">
     <label>{{idl_fields.au.profile.label}}</label>
     <img ng-show="field_doc.au.profile" 
@@ -206,7 +210,7 @@
   </div>
 </div>
 
-<div class="row pad-all-min form-group">
+<div class="row reg-field-row">
   <div class="col-md-3 reg-field-label">
   <label>{{idl_fields.au.expire_date.label}}</label>
     <img ng-show="field_doc.au.expire_date" 
@@ -224,7 +228,7 @@
 
 <!-- net_access_level -->
 
-<div class="row pad-all-min form-group">
+<div class="row reg-field-row">
   <div class="col-md-3 reg-field-label">
     <label>{{idl_fields.au.net_access_level.label}}</label>
     <img ng-show="field_doc.au.net_access_level" 
@@ -260,7 +264,7 @@
   <div class="col-md-6">[% l('User Settings') %]</div>
 </div>
 
-<div class="row pad-all-min form-group">
+<div class="row reg-field-row">
   <div class="col-md-3 reg-field-label">
     <label>{{user_setting_types['opac.default_phone'].label()}}</label>
   </div>
@@ -269,7 +273,7 @@
   </div>
 </div>
 
-<div class="row pad-all-min form-group">
+<div class="row reg-field-row">
   <div class="col-md-3 reg-field-label">
     <label>{{user_setting_types['opac.default_pickup_location'].label()}}</label>
   </div>
@@ -278,7 +282,7 @@
   </div>
 </div>
 
-<div class="row pad-all-min form-group">
+<div class="row reg-field-row">
   <div class="col-md-3 reg-field-label">
     <label>{{user_setting_types['circ.holds_behind_desk'].label()}}</label>
   </div>
@@ -289,7 +293,7 @@
   </div>
 </div>
 
-<div class="row pad-all-min form-group">
+<div class="row reg-field-row">
   <div class="col-md-3 reg-field-label">
     <label>[% l('Holds Notices') %]</label>
   </div>
@@ -309,7 +313,7 @@
   </div>
 </div>
 
-<div class="row pad-all-min form-group" ng-if="org_settings['sms.enable']">
+<div class="row reg-field-row" ng-if="org_settings['sms.enable']">
   <div class="col-md-3 reg-field-label">
     <label>[% l('Default SMS/Text Number') %]</label>
   </div>
@@ -318,7 +322,7 @@
   </div>
 </div>
 
-<div class="row pad-all-min form-group" ng-if="org_settings['sms.enable']">
+<div class="row reg-field-row" ng-if="org_settings['sms.enable']">
   <div class="col-md-3 reg-field-label">
     <label>[% l('Default SMS Carrier') %]</label>
   </div>
@@ -378,7 +382,7 @@
     <div class="col-md-6">[% l('Statistical Categories') %]</div>
 </div>
 
-<div class="row pad-all-min form-group" ng-repeat="cat in stat_cats">
+<div class="row reg-field-row" ng-repeat="cat in stat_cats">
   <div class="col-md-3 reg-field-label">
     <label>{{cat.name()}}</label>
   </div>
@@ -408,7 +412,7 @@
     <div class="col-md-6">[% l('Surveys') %]</div>
 </div>
 
-<div class="row pad-all-min form-group" ng-repeat="survey in surveys">
+<div class="row reg-field-row" ng-repeat="survey in surveys">
   <div class="col-md-3 reg-field-label">
     <label>{{survey.name()}}</label>
   </div>
diff --git a/Open-ILS/src/templates/staff/css/circ.css.tt2 b/Open-ILS/src/templates/staff/css/circ.css.tt2
index 4f83603..87a670c 100644
--- a/Open-ILS/src/templates/staff/css/circ.css.tt2
+++ b/Open-ILS/src/templates/staff/css/circ.css.tt2
@@ -58,6 +58,7 @@ but the ones I'm finding aren't quite cutting it..*/
 
 /* make all input widgets the same width, i.e. fill their column */
 
+.reg-field-input {font-weight: 700; }
 .reg-field-input input:not([type="checkbox"]) { width: 100%; }
 
 /* selector contents float left to allow depth-based left-padding */
@@ -101,6 +102,14 @@ but the ones I'm finding aren't quite cutting it..*/
     font-size: 100%;
 }
 
+.reg-field-row {
+    padding-bottom: 3px; 
+}
+
+.reg-field-row label {
+    font-weight: normal;
+}
+
 /* -- end patron registration -- */
 
 [%# 

commit b2834c2cd2e2f033fa36d8d4bf68751f5b063cf9
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Jun 17 19:58:29 2015 -0400

    LP#1464767 Patron edit billing => physical
    
    Move toward use of "Physical" label over "Billing" in browser client
    patron editor.
    
    See also LP#1068646
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 32907ce..eef3cca 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -347,7 +347,7 @@
           [% l('Mailing') %] <input type='checkbox'/>
         </span>
         <span class='pad-all-min'>
-          [% l('Billing') %] <input type='checkbox'/>
+          [% l('Physical') %] <input type='checkbox'/>
         </span>
         <span class='pad-all-min'>
           <button type="button" class="btn btn-danger">[% l('X') %]</button>

commit db936941e578a32ec4a12d5f4a33575ed262b571
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Jun 15 21:54:44 2015 -0400

    LP#1464767 Sort browser client org selector
    
    Sort each level of the shared org unit tree in the browser client by org
    unit shortname.  This primarily affects org unit selectors / dropdowns
    (unless otherwise sorted).
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/web/js/ui/default/staff/services/env.js b/Open-ILS/web/js/ui/default/staff/services/env.js
index 25198fa..ad41fc1 100644
--- a/Open-ILS/web/js/ui/default/staff/services/env.js
+++ b/Open-ILS/web/js/ui/default/staff/services/env.js
@@ -146,10 +146,19 @@ function($q,  $window , egAuth,  egPCRUD,  egIDL) {
                 return $q.when(tree);
             }
 
+            // sort orgs at each level by shortname
+            function sort_aou(node) {
+                node.children(node.children().sort(function(a, b) {
+                    return a.shortname() < b.shortname() ? -1 : 1;
+                }));
+                angular.forEach(node.children(), sort_aou);
+            }
+
             return egPCRUD.search('aou', {parent_ou : null}, 
                 {flesh : -1, flesh_fields : {aou : ['children', 'ou_type']}}
             ).then(
                 function(tree) {
+                    sort_aou(tree);
                     $window.sessionStorage.setItem(
                         'eg.env.aou.tree', js2JSON(tree));
                     service.absorbTree(tree, 'aou')

commit eb48b3a3ce62807ea1f0a74220e2242c6ef47225
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Jun 3 21:43:45 2015 -0400

    LP#1452950 patron reg. UI improvements / repairs
    
     * floating save, clone, etc pane arranged vertically with less padding.
     * reduce vertical spacing between fields
     * alert_message field rendered as textarea
     * avoid showing 'Example:' label when no phone example exists.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 17dde5e..32907ce 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -6,16 +6,18 @@
 <div id="reg-control-actions">
   <div class="pad-all-min">
     <button type="button" class="btn btn-default">[% l('Save') %]</button>
+  </div>
+  <div class="pad-all-min">
     <button type="button" class="btn btn-default">[% l('Save & Clone') %]</button>
   </div>
   <div class="pad-all-min" ng-show="!display_required_fields">
-    <a href>[% l('Show Only Required Fields') %]</a>
+    <a href>[% l('Required Fields') %]</a>
   </div>
   <div class="pad-all-min" ng-show="!display_suggested_fields">
-    <a href>[% l('Show Suggested Fields') %]</a>
+    <a href>[% l('Suggested Fields') %]</a>
   </div>
   <div class="pad-all-min" ng-show="!display_all_fields">
-    <a href>[% l('Show All Fields') %]</a>
+    <a href>[% l('All Fields') %]</a>
   </div>
   <fieldset id="reg-field-doc" ng-show="selected_field_doc">
     <legend>
@@ -31,7 +33,7 @@
 
   IF NOT input_type; input_type = 'text'; END %] 
 
-<div class="row pad-all-min2 form-group">
+<div class="row form-group">
 
   <div class="col-md-3 reg-field-label"> <!-- field label -->
 
@@ -45,9 +47,9 @@
 
   <div class="col-md-3 reg-field-input"> <!-- field form input -->
 
-  [%
-    model = path ? 'patron.' _ path _ '.' _ field : 'patron.' _ field;
-      IF input_type == 'checkbox' %]
+  [% model = path ? 'patron.' _ path _ '.' _ field : 'patron.' _ field %]
+
+  [% IF input_type == 'checkbox' %]
 
     <div class='checkbox'>
       <input type='checkbox' ng-model='[% model %]'/>
@@ -56,11 +58,17 @@
   [% ELSE %]
     <!-- text / number input -->
 
-    <input type="[% input_type %]" 
-      class="form-control" ng-model="[% model %]"/>
+    [% IF field == 'alert_message' %]
+      <textarea class="form-control" ng-model="[% model %]"/>
+    [% ELSE %]
+      <input type="[% input_type %]" 
+        class="form-control" ng-model="[% model %]"/>
+    [% END %]
   [% END %]
+
   </div>
 
+  <!-- supplemental actions and example text -->
   <div class="col-md-3 patron-reg-example">
 
     [% IF field == 'barcode' %]
@@ -85,7 +93,7 @@
 
       [% IF field.match('phone') %]
         <!-- phones have a fall-through example option -->
-        <span ng-if="![% set_str %]">
+        <span ng-if="![% set_str %] && org_settings['ui.patron.edit.phone.example']">
           [% l('Example: [_1]', 
            "{{org_settings['ui.patron.edit.phone.example']}}") %]
         </span>
diff --git a/Open-ILS/src/templates/staff/css/circ.css.tt2 b/Open-ILS/src/templates/staff/css/circ.css.tt2
index bf9d834..4f83603 100644
--- a/Open-ILS/src/templates/staff/css/circ.css.tt2
+++ b/Open-ILS/src/templates/staff/css/circ.css.tt2
@@ -79,13 +79,13 @@ but the ones I'm finding aren't quite cutting it..*/
 #reg-control-actions {
     position: fixed;
     top:160px;
-    right:30px;
-    width:300px;
+    right:10px;
+    /*width:200px;*/
     border:2px dashed #d9e8f9;
     -moz-border-radius: 10px;
     font-weight: bold;
-    padding: 20px;
-    margin-top: 20px;
+    padding: 10px;
+    margin-top: 10px;
 }
 
 #reg-field-doc {

commit ea9fa67caf9af12321ceb6c29ea9942895ce4608
Author: Bill Erickson <berickxx at gmail.com>
Date:   Sun May 3 21:19:51 2015 -0400

    LP#1452950 angularize patron registration phase I
    
    Replace legacy Dojo patron registration / edit UI's in the browser
    client with an initial cut of an Angular version.  For this commit, the
    UI is basically a wireframe, but the selectors display values and
    most fields display the correct values set on the patron.
    
    No save or clone etc. operations or data validation are functional.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/staff/circ/patron/index.tt2 b/Open-ILS/src/templates/staff/circ/patron/index.tt2
index bb0d2f8..3234b95 100644
--- a/Open-ILS/src/templates/staff/circ/patron/index.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/index.tt2
@@ -17,6 +17,7 @@
 [% INCLUDE 'staff/circ/share/hold_strings.tt2' %]
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/record.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/app.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/regctl.js"></script>
 
 <!-- load the rest on demand? -->
 
diff --git a/Open-ILS/src/templates/staff/circ/patron/register.tt2 b/Open-ILS/src/templates/staff/circ/patron/register.tt2
index 4a3c9ce..a0b2af4 100644
--- a/Open-ILS/src/templates/staff/circ/patron/register.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/register.tt2
@@ -5,8 +5,9 @@
 %]
 
 [% 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/services/ui.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/register.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/regctl.js"></script>
 <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/circ.css" />
 [% END %]
 
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
index 3f85366..17dde5e 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
@@ -1,2 +1,423 @@
-<eg-embed-frame url="patron_edit_url" handlers="funcs"></eg-embed-frame>
+[% DOC_IMG = '/images/question-mark.png' %]
+
+<div class="strong-text-2">[% l('Patron Edit') %]</div>
+
+<!-- IDL field documentation window -->
+<div id="reg-control-actions">
+  <div class="pad-all-min">
+    <button type="button" class="btn btn-default">[% l('Save') %]</button>
+    <button type="button" class="btn btn-default">[% l('Save & Clone') %]</button>
+  </div>
+  <div class="pad-all-min" ng-show="!display_required_fields">
+    <a href>[% l('Show Only Required Fields') %]</a>
+  </div>
+  <div class="pad-all-min" ng-show="!display_suggested_fields">
+    <a href>[% l('Show Suggested Fields') %]</a>
+  </div>
+  <div class="pad-all-min" ng-show="!display_all_fields">
+    <a href>[% l('Show All Fields') %]</a>
+  </div>
+  <fieldset id="reg-field-doc" ng-show="selected_field_doc">
+    <legend>
+    {{idl_fields[selected_field_doc.fm_class()][selected_field_doc.field()].label}}
+    </legend>
+    <div>{{selected_field_doc.string()}}</div>
+  </fieldset>
+</div>
+
+[% MACRO formfield(cls, field, path, input_type) BLOCK;
+
+  # input field generator for common text/number/checkbox fields
+
+  IF NOT input_type; input_type = 'text'; END %] 
+
+<div class="row pad-all-min2 form-group">
+
+  <div class="col-md-3 reg-field-label"> <!-- field label -->
+
+  <label>{{idl_fields.[% cls %].[% field %].label}}</label>
+
+  <!-- field documentation img/link -->
+  <img ng-show="field_doc.[% cls %].[% field %]" 
+    ng-click="selected_field_doc=field_doc.[% cls %].[% field %]"
+    src='[% DOC_IMG %]'></img>
+  </div>
+
+  <div class="col-md-3 reg-field-input"> <!-- field form input -->
+
+  [%
+    model = path ? 'patron.' _ path _ '.' _ field : 'patron.' _ field;
+      IF input_type == 'checkbox' %]
+
+    <div class='checkbox'>
+      <input type='checkbox' ng-model='[% model %]'/>
+    </div>
+
+  [% ELSE %]
+    <!-- text / number input -->
+
+    <input type="[% input_type %]" 
+      class="form-control" ng-model="[% model %]"/>
+  [% END %]
+  </div>
+
+  <div class="col-md-3 patron-reg-example">
+
+    [% IF field == 'barcode' %]
+
+      <button class="btn btn-default">[% l('Replace Barcode') %]</button>
+      <button class="btn btn-default">[% l('See All') %]</button>
+
+    [% ELSIF field == 'password' %]
+
+      <button class="btn btn-default">[% l('Generate Password') %]</button>
+
+    [% ELSE %]
+
+      <!-- example strings -->
+
+      [% set_str = "org_settings['ui.patron.edit." _ 
+          cls _ "." _ field _ ".example']"; %]
+
+      <span ng-if="[% set_str %]">
+        [% l('Example: [_1]', "{{" _ set_str _ "}}") %]
+      </span>
+
+      [% IF field.match('phone') %]
+        <!-- phones have a fall-through example option -->
+        <span ng-if="![% set_str %]">
+          [% l('Example: [_1]', 
+           "{{org_settings['ui.patron.edit.phone.example']}}") %]
+        </span>
+      [% END %]
+    [% END %]
+
+  </div>
+</div>
+[% END %]
+
+[% formfield('ac', 'barcode', 'card') %]
+[% formfield('au', 'usrname') %]
+[% formfield('au', 'passwd') %]
+[% formfield('au', 'prefix') %]
+[% formfield('au', 'first_given_name') %]
+[% formfield('au', 'second_given_name') %]
+[% formfield('au', 'family_name') %]
+[% formfield('au', 'suffix') %]
+[% formfield('au', 'alias') %]
+
+<div class="row pad-all-min form-group">
+  <div class="col-md-3 reg-field-label">
+    <label>{{idl_fields.au.dob.label}}</label>
+    <img ng-show="field_doc.au.dob" 
+      ng-click="selected_field_doc=field_doc.au.dob"
+      src='[% DOC_IMG %]'></img>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <input eg-date-input 
+      class="form-control" ng-model="patron.dob"/>
+  </div>
+</div>
+
+[% formfield('au', 'juvenile', '', 'checkbox') %]
+
+<!-- ident_type -->
+
+<div class="row pad-all-min form-group">
+  <div class="col-md-3 reg-field-label">
+    <label>{{idl_fields.au.ident_type.label}}</label>
+    <img ng-show="field_doc.au.ident_type" 
+      ng-click="selected_field_doc=field_doc.au.ident_type"
+      src='[% DOC_IMG %]'></img>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <div class="btn-group" dropdown>
+      <button type="button" class="btn btn-default dropdown-toggle">
+        <span style="padding-right: 5px;">
+          {{patron.ident_type.name() || "[% l('Primary Ident Type') %]"}}
+        </span>
+        <span class="caret"></span>
+      </button>
+      <ul class="dropdown-menu">
+        <li ng-repeat="type in ident_types">
+          <a href ng-click="patron.ident_type = type">{{type.name()}}</a>
+        </li>
+      </ul>
+    </div>
+  </div>
+</div>
+
+
+[% formfield('au', 'ident_value') %]
+[% formfield('au', 'ident_value2') %]
+[% formfield('au', 'email', '', 'email') %]
+[% formfield('au', 'day_phone') %]
+[% formfield('au', 'evening_phone') %]
+[% formfield('au', 'other_phone') %]
+
+<!-- home org unit selector -->
+
+<div class="row pad-all-min form-group">
+  <div class="col-md-3 reg-field-label">
+    <label>{{idl_fields.au.home_ou.label}}</label>
+    <img ng-show="field_doc.au.home_ou" 
+      ng-click="selected_field_doc=field_doc.au.home_ou"
+      src='[% DOC_IMG %]'></img>
+    </div>
+    <div class="col-md-3 reg-field-input">
+      <eg-org-selector selected="patron.home_ou" onchange="">
+      </eg-org-selector>
+  </div>
+</div>
+
+<!-- profile selector -->
+
+<div class="row pad-all-min form-group">
+  <div class="col-md-3 reg-field-label">
+    <label>{{idl_fields.au.profile.label}}</label>
+    <img ng-show="field_doc.au.profile" 
+      ng-click="selected_field_doc=field_doc.au.profile"
+      src='[% DOC_IMG %]'></img>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <div class="btn-group" dropdown>
+      <button type="button" class="btn btn-default dropdown-toggle">
+        <span style="padding-right: 5px;">
+          {{patron.profile.name() || "[% l('Profile Group') %]"}}
+        </span>
+        <span class="caret"></span>
+      </button>
+      <ul class="dropdown-menu">
+        <li ng-repeat="grp in profiles">
+          <a href 
+            style="padding-left: {{pgt_depth(grp) * 10 + 5}}px"
+            ng-click="patron.profile = grp">{{grp.name()}}</a>
+        </li>
+      </ul>
+    </div>
+  </div>
+</div>
+
+<div class="row pad-all-min form-group">
+  <div class="col-md-3 reg-field-label">
+  <label>{{idl_fields.au.expire_date.label}}</label>
+    <img ng-show="field_doc.au.expire_date" 
+    ng-click="selected_field_doc=field_doc.au.expire_date"
+    src='[% DOC_IMG %]'></img>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <input eg-date-input 
+      class="form-control" ng-model="patron.expire_date"/>
+  </div>
+  <div class="col-md-3">
+    <button class="btn btn-default">[% l('Update Expire Date') %]</button>
+  </div>
+</div>
+
+<!-- net_access_level -->
+
+<div class="row pad-all-min form-group">
+  <div class="col-md-3 reg-field-label">
+    <label>{{idl_fields.au.net_access_level.label}}</label>
+    <img ng-show="field_doc.au.net_access_level" 
+      ng-click="selected_field_doc=field_doc.au.net_access_level"
+      src='[% DOC_IMG %]'></img>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <div class="btn-group" dropdown>
+      <button type="button" class="btn btn-default dropdown-toggle">
+        <span style="padding-right: 5px;">
+          {{patron.net_access_level.name() || "[% l('Net Access Level') %]"}}
+        </span>
+        <span class="caret"></span>
+      </button>
+      <ul class="dropdown-menu">
+        <li ng-repeat="level in net_access_levels">
+          <a href 
+            ng-click="patron.net_access_level = level">{{level.name()}}</a>
+        </li>
+      </ul>
+    </div>
+  </div>
+</div>
+
+[% formfield('au', 'active', '', 'checkbox') %]
+[% formfield('au', 'barred', '', 'checkbox') %]
+[% formfield('au', 'master_account', '', 'checkbox') %]
+[% formfield('au', 'claims_returned_count', '', 'number') %]
+[% formfield('au', 'claims_never_checked_out_count', '', 'number') %]
+[% formfield('au', 'alert_message') %]
+
+<div class="alert alert-success row" role="alert">
+  <div class="col-md-6">[% l('User Settings') %]</div>
+</div>
+
+<div class="row pad-all-min form-group">
+  <div class="col-md-3 reg-field-label">
+    <label>{{user_setting_types['opac.default_phone'].label()}}</label>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <input type='text' ng-model="user_settings['opac.default_phone']"/>
+  </div>
+</div>
+
+<div class="row pad-all-min form-group">
+  <div class="col-md-3 reg-field-label">
+    <label>{{user_setting_types['opac.default_pickup_location'].label()}}</label>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <eg-org-selector selected="patron.home_ou" onchange=""></eg-org-selector>
+  </div>
+</div>
+
+<div class="row pad-all-min form-group">
+  <div class="col-md-3 reg-field-label">
+    <label>{{user_setting_types['circ.holds_behind_desk'].label()}}</label>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <div class='checkbox'>
+      <input type='checkbox' ng-model="user_settings['circ.holds_behind_desk']"/>
+    </div>
+  </div>
+</div>
+
+<div class="row pad-all-min form-group">
+  <div class="col-md-3 reg-field-label">
+    <label>[% l('Holds Notices') %]</label>
+  </div>
+  <div class="col-md-3 reg-field-input flex-row">
+    <div class='flex-cell'>
+      <input type='checkbox' ng-model="hold_notify_phone"/>
+      [% l('Phone') %]
+    </div>
+    <div class='flex-cell'>
+      <input type='checkbox' ng-model="hold_notify_email"/>
+      [% l('Email') %]
+    </div>
+    <div class='flex-cell'>
+      <input type='checkbox' ng-model="hold_notify_sms"/>
+      [% l('SMS') %]
+    </div>
+  </div>
+</div>
+
+<div class="row pad-all-min form-group" ng-if="org_settings['sms.enable']">
+  <div class="col-md-3 reg-field-label">
+    <label>[% l('Default SMS/Text Number') %]</label>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <input type='text'/>
+  </div>
+</div>
+
+<div class="row pad-all-min form-group" ng-if="org_settings['sms.enable']">
+  <div class="col-md-3 reg-field-label">
+    <label>[% l('Default SMS Carrier') %]</label>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <div class="btn-group" dropdown>
+      <button type="button" class="btn btn-default dropdown-toggle">
+        <span style="padding-right: 5px;"></span>
+        <span class="caret"></span>
+      </button>
+      <ul class="dropdown-menu">
+        <li ng-repeat="carrier in sms_carriers">
+          <a href 
+            ng-click="user_settings['opac.default_sms_carrier'] = carrier">
+                {{carrier.name()}}
+          </a>
+        </li>
+      </ul>
+    </div>
+  </div>
+</div>
+
+<div class="alert alert-success row" role="alert">
+    <div class="col-md-3">[% l('Address') %]</div>
+    <div class="col-md-3">
+        <span class='pad-all-min'>
+          [% l('Mailing') %] <input type='checkbox'/>
+        </span>
+        <span class='pad-all-min'>
+          [% l('Billing') %] <input type='checkbox'/>
+        </span>
+        <span class='pad-all-min'>
+          <button type="button" class="btn btn-danger">[% l('X') %]</button>
+        </span>
+    </div>
+</div>
+
+<!-- addresses -->
+
+[% formfield('aua', 'address_type', 'mailing_address') %]
+[% formfield('aua', 'post_code', 'mailing_address') %]
+[% formfield('aua', 'street1', 'mailing_address') %]
+[% formfield('aua', 'street2', 'mailing_address') %]
+[% formfield('aua', 'city', 'mailing_address') %]
+[% formfield('aua', 'county', 'mailing_address') %]
+[% formfield('aua', 'state', 'mailing_address') %]
+[% formfield('aua', 'country', 'mailing_address') %]
+[% formfield('aua', 'valid', 'mailing_address', 'checkbox') %]
+[% formfield('aua', 'within_city_limits', 'mailing_address', 'checkbox') %]
+
+<div class="row">
+  <button type="button" class="btn btn-success">[% l('New Address') %]</button>
+</div>
+
+<!-- pending address -->
+
+<div class="alert alert-success row" role="alert" ng-if="stat_cats.length > 0">
+    <div class="col-md-6">[% l('Statistical Categories') %]</div>
+</div>
+
+<div class="row pad-all-min form-group" ng-repeat="cat in stat_cats">
+  <div class="col-md-3 reg-field-label">
+    <label>{{cat.name()}}</label>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <div ng-if="cat.entries().length == 0">
+      <input type="text" class="form-control"/>
+    </div>
+    <div ng-if="cat.entries().length != 0">
+      <div class="btn-group" dropdown>
+        <button type="button" class="btn btn-default dropdown-toggle">
+          <span style="padding-right: 5px;"></span>
+          <span class="caret"></span>
+        </button>
+        <ul class="dropdown-menu">
+          <li ng-repeat="entry in cat.entries()">
+            <a href ng-click=""> {{entry.value()}} </a>
+          </li>
+        </ul>
+      </div>
+    </div>
+  </div>
+</div>
+
+<!-- surveys -->
+
+<div class="alert alert-success row" role="alert" ng-if="surveys.length > 0">
+    <div class="col-md-6">[% l('Surveys') %]</div>
+</div>
+
+<div class="row pad-all-min form-group" ng-repeat="survey in surveys">
+  <div class="col-md-3 reg-field-label">
+    <label>{{survey.name()}}</label>
+  </div>
+  <div class="col-md-3 reg-field-input">
+    <div class="btn-group" dropdown>
+      <button type="button" class="btn btn-default dropdown-toggle">
+        <span style="padding-right: 5px;"></span>
+        <span class="caret"></span>
+      </button>
+      <ul class="dropdown-menu">
+        <li ng-repeat="question in survey.questions()">
+          <a href ng-click=""> {{question.question()}} </a>
+        </li>
+      </ul>
+    </div>
+  </div>
+</div>
+
+
 
diff --git a/Open-ILS/src/templates/staff/css/circ.css.tt2 b/Open-ILS/src/templates/staff/css/circ.css.tt2
index 8d6c139..bf9d834 100644
--- a/Open-ILS/src/templates/staff/css/circ.css.tt2
+++ b/Open-ILS/src/templates/staff/css/circ.css.tt2
@@ -54,6 +54,54 @@ but the ones I'm finding aren't quite cutting it..*/
   border-bottom: 1px solid #CCC;
 }
 
+/* -- patron registration -- */
+
+/* make all input widgets the same width, i.e. fill their column */
+
+.reg-field-input input:not([type="checkbox"]) { width: 100%; }
+
+/* selector contents float left to allow depth-based left-padding */
+.reg-field-input .eg-org-selector,
+.reg-field-input .btn-group {
+  width: 100%; 
+  text-align: left;
+}
+
+/* selector button labels float right */
+.reg-field-input .eg-org-selector button,
+.reg-field-input .btn-group > button {
+  width: 100%; 
+  text-align: right;
+}
+
+
+/* floating div along top-right with field documentation */
+#reg-control-actions {
+    position: fixed;
+    top:160px;
+    right:30px;
+    width:300px;
+    border:2px dashed #d9e8f9;
+    -moz-border-radius: 10px;
+    font-weight: bold;
+    padding: 20px;
+    margin-top: 20px;
+}
+
+#reg-field-doc {
+    border:2px dashed #d9e8f9;
+    -moz-border-radius: 10px;
+    font-weight: bold;
+    padding: 20px;
+    margin-top: 20px;
+}
+
+#reg-field-doc legend {
+    /* otherwise the font size is quite large */
+    font-size: 100%;
+}
+
+/* -- end patron registration -- */
 
 [%# 
 vim: ft=css 
diff --git a/Open-ILS/src/templates/staff/css/style.css.tt2 b/Open-ILS/src/templates/staff/css/style.css.tt2
index 0c21913..5c367b8 100644
--- a/Open-ILS/src/templates/staff/css/style.css.tt2
+++ b/Open-ILS/src/templates/staff/css/style.css.tt2
@@ -116,6 +116,7 @@ table.list tr.selected td { /* deprecated? */
 .pad-left {padding-left: 10px;}
 .pad-right {padding-right: 10px;}
 .pad-all-min {padding : 5px; }
+.pad-all-min2 {padding : 2px; }
 .pad-all {padding : 10px; }
 
 #print-div { display: none; }
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 d5fb95d..46aab7c 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
@@ -159,7 +159,7 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap',
 
     $routeProvider.when('/circ/patron/:id/edit', {
         templateUrl: './circ/patron/t_edit',
-        controller: 'PatronEditCtrl',
+        controller: 'PatronRegCtrl',
         resolve : resolver
     });
 
@@ -1167,26 +1167,6 @@ function($scope , $q , $routeParams,  egCore , $modal , patronSvc , egCirc) {
 
 
 /**
- * Link to patron edit UI
- */
-.controller('PatronEditCtrl',
-       ['$scope','$routeParams','$location','egCore','patronSvc',
-function($scope,  $routeParams,  $location , egCore , patronSvc) {
-    $scope.initTab('edit', $routeParams.id);
-
-    var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/register');
-    url += '?usr=' + encodeURIComponent($routeParams.id);
-
-    $scope.funcs = {
-        on_save : function() {
-            patronSvc.refreshPrimary();
-        }
-    }
-
-    $scope.patron_edit_url = url;
-}])
-
-/**
  * Credentials tester
  */
 .controller('PatronVerifyCredentialsCtrl',
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
new file mode 100644
index 0000000..b940514
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
@@ -0,0 +1,364 @@
+
+angular.module('egCoreMod')
+// toss tihs onto egCoreMod since the page app may vary
+
+.factory('patronRegSvc', ['$q', 'egCore', function($q, egCore) {
+
+    var service = {
+        field_doc : {},             // config.idl_field_doc
+        profiles : [],              // permission groups
+        sms_carriers : [],
+        user_settings : {},         // applied user settings
+        user_setting_types : {},    // config.usr_setting_type
+        modified_user_settings : {} // settings modifed this session
+    };
+
+    // launch a series of parallel data retrieval calls
+    service.init = function(scope) {
+        return $q.all([
+            service.get_field_doc(),
+            service.get_perm_groups(),
+            service.get_ident_types(),
+            service.get_user_settings(),
+            service.get_org_settings(),
+            service.get_stat_cats(),
+            service.get_surveys(),
+            service.get_net_access_levels()
+        ]);
+    };
+
+    service.get_surveys = function() {
+        var org_ids = egCore.org.ancestors(egCore.auth.user().ws_ou(), true);
+
+        return egCore.pcrud.search('asv', 
+            {owner : org_ids}, 
+            {flesh : 1, flesh_fields : {asv : ['questions']}}, 
+            {atomic : true}
+        ).then(function(surveys) {
+            service.surveys = surveys;
+        });
+    }
+
+    service.get_stat_cats = function() {
+        return egCore.net.request(
+            'open-ils.circ',
+            'open-ils.circ.stat_cat.actor.retrieve.all',
+            egCore.auth.token(), egCore.auth.user().ws_ou()
+        ).then(function(cats) {
+            service.stat_cats = cats;
+        });
+    };
+
+    service.get_org_settings = function() {
+        return egCore.org.settings([
+            'global.password_regex',
+            'global.juvenile_age_threshold',
+            'patron.password.use_phone',
+            'ui.patron.default_inet_access_level',
+            'ui.patron.default_ident_type',
+            'ui.patron.default_country',
+            'ui.patron.registration.require_address',
+            'circ.holds.behind_desk_pickup_supported',
+            'circ.patron_edit.clone.copy_address',
+            'ui.patron.edit.au.prefix.require',
+            'ui.patron.edit.au.prefix.show',
+            'ui.patron.edit.au.prefix.suggest',
+            'ui.patron.edit.ac.barcode.regex',
+            'ui.patron.edit.au.second_given_name.show',
+            'ui.patron.edit.au.second_given_name.suggest',
+            'ui.patron.edit.au.suffix.show',
+            'ui.patron.edit.au.suffix.suggest',
+            'ui.patron.edit.au.alias.show',
+            'ui.patron.edit.au.alias.suggest',
+            'ui.patron.edit.au.dob.require',
+            'ui.patron.edit.au.dob.show',
+            'ui.patron.edit.au.dob.suggest',
+            'ui.patron.edit.au.dob.calendar',
+            'ui.patron.edit.au.juvenile.show',
+            'ui.patron.edit.au.juvenile.suggest',
+            'ui.patron.edit.au.ident_value.show',
+            'ui.patron.edit.au.ident_value.suggest',
+            'ui.patron.edit.au.ident_value2.show',
+            'ui.patron.edit.au.ident_value2.suggest',
+            'ui.patron.edit.au.email.require',
+            'ui.patron.edit.au.email.show',
+            'ui.patron.edit.au.email.suggest',
+            'ui.patron.edit.au.email.regex',
+            'ui.patron.edit.au.email.example',
+            'ui.patron.edit.au.day_phone.require',
+            'ui.patron.edit.au.day_phone.show',
+            'ui.patron.edit.au.day_phone.suggest',
+            'ui.patron.edit.au.day_phone.regex',
+            'ui.patron.edit.au.day_phone.example',
+            'ui.patron.edit.au.evening_phone.require',
+            'ui.patron.edit.au.evening_phone.show',
+            'ui.patron.edit.au.evening_phone.suggest',
+            'ui.patron.edit.au.evening_phone.regex',
+            'ui.patron.edit.au.evening_phone.example',
+            'ui.patron.edit.au.other_phone.require',
+            'ui.patron.edit.au.other_phone.show',
+            'ui.patron.edit.au.other_phone.suggest',
+            'ui.patron.edit.au.other_phone.regex',
+            'ui.patron.edit.au.other_phone.example',
+            'ui.patron.edit.phone.regex',
+            'ui.patron.edit.phone.example',
+            'ui.patron.edit.au.active.show',
+            'ui.patron.edit.au.active.suggest',
+            'ui.patron.edit.au.barred.show',
+            'ui.patron.edit.au.barred.suggest',
+            'ui.patron.edit.au.master_account.show',
+            'ui.patron.edit.au.master_account.suggest',
+            'ui.patron.edit.au.claims_returned_count.show',
+            'ui.patron.edit.au.claims_returned_count.suggest',
+            'ui.patron.edit.au.claims_never_checked_out_count.show',
+            'ui.patron.edit.au.claims_never_checked_out_count.suggest',
+            'ui.patron.edit.au.alert_message.show',
+            'ui.patron.edit.au.alert_message.suggest',
+            'ui.patron.edit.aua.post_code.regex',
+            'ui.patron.edit.aua.post_code.example',
+            'ui.patron.edit.aua.county.require',
+            'format.date',
+            'ui.patron.edit.default_suggested',
+            'opac.barcode_regex',
+            'opac.username_regex',
+            'sms.enable',
+            'ui.patron.edit.aua.state.require',
+            'ui.patron.edit.aua.state.suggest',
+            'ui.patron.edit.aua.state.show'
+        ]).then(function(settings) {
+            service.org_settings = settings;
+            return service.process_org_settings(settings);
+        });
+    };
+
+    // some org settings require the retrieval of additional data
+    service.process_org_settings = function(settings) {
+
+        if (!settings['sms.enable']) {
+            return $q.when();
+        }
+
+        return egCore.pcrud.search('csc', 
+            {active: 'true'}, 
+            {'order_by':[
+                {'class':'csc', 'field':'name'},
+                {'class':'csc', 'field':'region'}
+            ]},
+            {atomic : true}
+        ).then(function(carriers) {
+            service.sms_carriers = carriers;
+        });
+    };
+
+    service.get_ident_types = function() {
+        return egCore.pcrud.retrieveAll('cit', {}, {atomic : true})
+        .then(function(types) { service.ident_types = types });
+    };
+
+    service.get_net_access_levels = function() {
+        return egCore.pcrud.retrieveAll('cnal', {}, {atomic : true})
+        .then(function(levels) { service.net_access_levels = levels });
+    }
+
+    service.get_perm_groups = function() {
+        if (egCore.env.pgt) {
+            service.profiles = egCore.env.pgt.list;
+            return $q.when();
+        } else {
+            return egCore.pcrud.search('pgt', {parent : null}, 
+                {flesh : -1, flesh_fields : {pgt : ['children']}}
+            ).then(
+                function(tree) {
+                    egCore.env.absorbTree(tree, 'pgt')
+                    service.profiles = egCore.env.pgt.list;
+                }
+            );
+        }
+    }
+
+    service.get_field_doc = function() {
+
+        return egCore.pcrud.search('fdoc', {
+            fm_class: ['au', 'ac', 'aua', 'actsc', 'asv', 'asvq', 'asva']})
+        .then(null, null, function(doc) {
+            if (!service.field_doc[doc.fm_class()]) {
+                service.field_doc[doc.fm_class()] = {};
+            }
+            service.field_doc[doc.fm_class()][doc.field()] = doc;
+        });
+    };
+
+    service.get_user_settings = function() {
+        var org_ids = egCore.org.ancestors(egCore.auth.user().ws_ou(), true);
+
+        return egCore.pcrud.search('cust', {
+            '-or' : [
+                {name : [ // common user settings
+                    'circ.holds_behind_desk', 
+                    'circ.collections.exempt', 
+                    'opac.hold_notify', 
+                    'opac.default_phone', 
+                    'opac.default_pickup_location', 
+                    'opac.default_sms_carrier', 
+                    'opac.default_sms_notify']}, 
+                {name : { // opt-in notification user settings
+                    'in': {
+                        select : {atevdef : ['opt_in_setting']}, 
+                        from : 'atevdef',
+                        // we only care about opt-in settings for 
+                        // event_defs our users encounter
+                        where : {'+atevdef' : {owner : org_ids}}
+                    }
+                }}
+            ]
+        }, {}, {atomic : true}).then(function(setting_types) {
+
+            angular.forEach(setting_types, function(stype) {
+                service.user_setting_types[stype.name()] = stype;
+            });
+
+            if(service.patron_id) {
+                // retrieve applied values for the current user 
+                // for the setting types we care about.
+
+                var setting_names = 
+                    setting_types.map(function(obj) { return obj.name() });
+
+                return egCore.net.request(
+                    'open-ils.actor', 
+                    'open-ils.actor.patron.settings.retrieve.authoritative',
+                    egCore.auth.token(),
+                    service.patron_id,
+                    setting_names
+                ).then(function(settings) {
+                    service.user_settings = settings;
+                });
+            }
+
+            // apply default user setting values
+            angular.forEach(setting_types, function(stype, index) {
+                if (stype.reg_default() != undefined) {
+                    service.modified_user_settings[setting.name()] = 
+                        service.user_settings[setting.name()] = 
+                        setting.reg_default();
+                }
+            });
+        });
+    }
+
+    service.init_patron = function(current) {
+
+        if (!current)
+            return service.init_new_patron();
+
+        service.patron = current;
+        return service.init_existing_patron(current)
+    }
+
+    /*
+     * Existing patron objects reqire some data munging before insertion
+     * into the scope.
+     *
+     * 1. Turn everything into a hash
+     * 2. ... Except certain fields (selectors) whose widgets require objects
+     * 3. Bools must be Boolean, not t/f.
+     */
+    service.init_existing_patron = function(current) {
+
+        var patron = egCore.idl.toHash(current);
+
+        patron.home_ou = egCore.org.get(patron.home_ou.id);
+        patron.expire_date = new Date(Date.parse(patron.expire_date));
+        patron.dob = new Date(Date.parse(patron.dob));
+        patron.profile = current.profile(); // pre-hash version
+        patron.net_access_level = current.net_access_level();
+        patron.ident_type = current.ident_type();
+
+        angular.forEach(
+            ['juvenile', 'barred', 'active', 'master_account'],
+            function(field) { patron[field] = patron[field] == 't'; }
+        );
+
+        angular.forEach(patron.addresses, function(addr) {
+            addr.valid = addr.valid == 't';
+            addr.within_city_limits = addr.within_city_limits == 't';
+        });
+
+        return patron;
+    }
+
+    service.init_new_patron = function() {
+
+        var addr = {
+            valid : true,
+            within_city_limits : true
+            // default state, etc.
+        };
+
+        return {
+            isnew : true,
+            active : true,
+            card : {},
+            home_ou : egCore.org.get(egCore.auth.user().ws_ou()),
+            // TODO default profile group?
+            mailing_address : addr,
+            addresses : [addr]
+        };
+    }
+
+    return service;
+}]);
+
+
+function PatronRegCtrl($scope, $routeParams, 
+    $q, egCore, patronSvc, patronRegSvc) {
+
+    $scope.clone_id = $routeParams.clone_id;
+    $scope.stage_username = $routeParams.stage_username;
+    $scope.patron_id = 
+        patronRegSvc.patron_id = $routeParams.edit_id || $routeParams.id;
+
+    $q.all([
+
+        $scope.initTab ? // initTab comes from patron app
+            $scope.initTab('edit', $routeParams.id) : $q.when(),
+
+        patronRegSvc.init()
+
+    ]).then(function() {
+        // called after initTab and patronRegSvc.init have completed
+
+        var prs = patronRegSvc; // brevity
+        // in standalone mode, we have no patronSvc
+        $scope.patron = prs.init_patron(patronSvc ? patronSvc.current : null);
+        $scope.field_doc = prs.field_doc;
+        $scope.profiles = prs.profiles;
+        $scope.ident_types = prs.ident_types;
+        $scope.net_access_levels = prs.net_access_levels;
+        $scope.user_settings = prs.user_settings;
+        $scope.user_setting_types = prs.user_setting_types;
+        $scope.modified_user_settings = prs.modified_user_settings;
+        $scope.org_settings = prs.org_settings;
+        $scope.sms_carriers = prs.sms_carriers;
+        $scope.stat_cats = prs.stat_cats;
+        $scope.surveys = prs.surveys;
+    });
+
+    // returns the tree depth of the selected profile group tree node.
+    $scope.pgt_depth = function(grp) {
+        var d = 0;
+        while (grp = egCore.env.pgt.map[grp.parent()]) d++;
+        return d;
+    }
+
+    // IDL fields used for labels in the UI.
+    $scope.idl_fields = {
+        au  : egCore.idl.classes.au.field_map,
+        ac  : egCore.idl.classes.ac.field_map,
+        aua : egCore.idl.classes.aua.field_map
+    };
+
+}
+
+
+// TODO: $inject controller params 
diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/register.js b/Open-ILS/web/js/ui/default/staff/circ/patron/register.js
index 13b4a41..49306f0 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/patron/register.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/patron/register.js
@@ -4,7 +4,7 @@
  * Search, checkout, items out, holds, bills, edit, etc.
  */
 
-angular.module('egPatronRegApp', ['ui.bootstrap','ngRoute','egCoreMod'])
+angular.module('egPatronRegApp', ['ui.bootstrap','ngRoute','egCoreMod', 'egUiMod'])
 
 
 .config(function($routeProvider, $locationProvider, $compileProvider) {
@@ -15,25 +15,25 @@ angular.module('egPatronRegApp', ['ui.bootstrap','ngRoute','egCoreMod'])
         ['egStartup', function(egStartup) {return egStartup.go()}]}
 
     $routeProvider.when('/circ/patron/register', {
-        template: '<eg-embed-frame url="reg_url"></eg-embed-frame>',
+        templateUrl: './circ/patron/t_edit',
         controller: 'PatronRegCtrl',
         resolve : resolver
     });
 
     $routeProvider.when('/circ/patron/register/stage/:stage_username', {
-        template: '<eg-embed-frame url="reg_url"></eg-embed-frame>',
+        templateUrl: './circ/patron/t_edit',
         controller: 'PatronRegCtrl',
         resolve : resolver
     });
 
     $routeProvider.when('/circ/patron/register/edit/:edit_id', {
-        template: '<eg-embed-frame url="reg_url"></eg-embed-frame>',
+        templateUrl: './circ/patron/t_edit',
         controller: 'PatronRegCtrl',
         resolve : resolver
     });
 
     $routeProvider.when('/circ/patron/register/clone/:clone_id', {
-        template: '<eg-embed-frame url="reg_url"></eg-embed-frame>',
+        templateUrl: './circ/patron/t_edit',
         controller: 'PatronRegCtrl',
         resolve : resolver
     });
@@ -41,32 +41,7 @@ angular.module('egPatronRegApp', ['ui.bootstrap','ngRoute','egCoreMod'])
     $routeProvider.otherwise({redirectTo : '/circ/patron/register'});
 })
 
+// dummy service so standalone patron editor can reference it
+.factory('patronSvc', function() {});
 
-/**
- * */
-.controller('PatronRegCtrl',
-       ['$scope','$routeParams','$location','egCore',
-function($scope , $routeParams , $location , egCore) {
-    
-
-    var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/register');
-
-    // since we don't store auth cookies, pass the cookie via URL
-    url += '?ses=' + egCore.auth.token();
-
-    if ($routeParams.stage_username) {
-        url += '&stage=' + encodeURIComponent($routeParams.stage_username);
-    }
-
-    if ($routeParams.edit_id) {
-        url += '&usr=' + encodeURIComponent($routeParams.edit_id);
-    }
-
-    if ($routeParams.clone_id) {
-        url += '&clone=' + encodeURIComponent($routeParams.clone_id);
-    }
 
-    // pass the reg URL into the scope, thus into the 
-    $scope.reg_url = url;
-}])
- 

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

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |    4 +-
 Open-ILS/src/templates/staff/base_js.tt2           |    8 +
 Open-ILS/src/templates/staff/circ/patron/index.tt2 |   25 +-
 .../templates/staff/circ/patron/reg_actions.tt2    |   34 +
 .../src/templates/staff/circ/patron/register.tt2   |   13 +-
 .../src/templates/staff/circ/patron/t_edit.tt2     |  643 +++++++++-
 .../staff/circ/patron/t_patron_cards_dialog.tt2    |   34 +
 .../staff/circ/patron/t_patron_groups_dialog.tt2   |   60 +
 .../src/templates/staff/circ/patron/t_summary.tt2  |    2 +-
 Open-ILS/src/templates/staff/css/circ.css.tt2      |   98 ++
 Open-ILS/src/templates/staff/css/style.css.tt2     |    8 +
 .../web/js/ui/default/staff/circ/patron/app.js     |   44 +-
 .../web/js/ui/default/staff/circ/patron/regctl.js  | 1464 ++++++++++++++++++++
 .../js/ui/default/staff/circ/patron/register.js    |   39 +-
 Open-ILS/web/js/ui/default/staff/services/env.js   |    9 +
 Open-ILS/web/js/ui/default/staff/services/grid.js  |    4 +-
 Open-ILS/web/js/ui/default/staff/services/idl.js   |   29 +
 Open-ILS/web/js/ui/default/staff/services/ui.js    |   53 +
 .../native_web_staff_client_patron_editor.adoc     |   13 +
 19 files changed, 2518 insertions(+), 66 deletions(-)
 create mode 100644 Open-ILS/src/templates/staff/circ/patron/reg_actions.tt2
 create mode 100644 Open-ILS/src/templates/staff/circ/patron/t_patron_cards_dialog.tt2
 create mode 100644 Open-ILS/src/templates/staff/circ/patron/t_patron_groups_dialog.tt2
 create mode 100644 Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
 create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/native_web_staff_client_patron_editor.adoc


hooks/post-receive
-- 
Evergreen ILS




More information about the open-ils-commits mailing list