LP#1842940: Don't allow self-edit or perm-restricted edit
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / patron / regctl.js
index 69577d3..1d34476 100644 (file)
@@ -2,12 +2,14 @@
 angular.module('egCoreMod')
 // toss tihs onto egCoreMod since the page app may vary
 
-.factory('patronRegSvc', ['$q', 'egCore', function($q, egCore) {
+.factory('patronRegSvc', ['$q', '$filter', 'egCore', 'egLovefield', function($q, $filter, egCore, egLovefield) {
 
     var service = {
         field_doc : {},            // config.idl_field_doc
         profiles : [],             // permission groups
+        profile_entries : [],      // permission gorup display entries
         edit_profiles : [],        // perm groups we can modify
+        edit_profile_entries : [], // perm group display entries we can modify
         sms_carriers : [],
         user_settings : {},        // applied user settings
         user_setting_types : {},   // config.usr_setting_type
@@ -22,29 +24,33 @@ angular.module('egCoreMod')
         init_done : false           // have we loaded our initialization data?
     };
 
-    // launch a series of parallel data retrieval calls
+    // 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(),
-            service.get_ident_types(),
+        // These are fetched with every instance of the page.
+        var page_data = [
             service.get_user_settings(),
-            service.get_org_settings(),
-            service.get_stat_cats(),
-            service.get_surveys(),
             service.get_clone_user(),
-            service.get_stage_user(),
-            service.get_net_access_levels()
-        ]);
+            service.get_stage_user()
+        ];
+
+        var common_data = [];
+        if (!service.init_done) {
+            // These are fetched with every instance of the app.
+            common_data = [
+                service.get_field_doc(),
+                service.get_perm_groups(),
+                service.get_perm_group_entries(),
+                service.get_ident_types(),
+                service.get_org_settings(),
+                service.get_stat_cats(),
+                service.get_surveys(),
+                service.get_net_access_levels()
+            ];
+            service.init_done = true;
+        }
+
+        return $q.all(common_data.concat(page_data));
     };
 
     service.get_clone_user = function() {
@@ -197,6 +203,44 @@ angular.module('egCoreMod')
         );
     }
 
+    service.set_edit_profile_entries = function() {
+        var all_app_perms = [];
+        var failed_perms = [];
+
+        // extract the application permissions
+        angular.forEach(service.profile_entries, function(entry) {
+            if (entry.grp().application_perm())
+                all_app_perms.push(entry.grp().application_perm());
+        });
+
+        // fill in service.edit_profiles by inspecting failed_perms
+        function traverse_grp_tree(entry, failed) {
+            failed = failed ||
+                failed_perms.indexOf(entry.grp().application_perm()) > -1;
+
+            if (!failed) service.edit_profile_entries.push(entry);
+
+            angular.forEach(
+                service.profile_entries.filter( // children of grp
+                    function(p) { return p.parent() == entry.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);
+                });
+
+                angular.forEach(egCore.env.pgtde.tree, function(tree) {
+                    traverse_grp_tree(tree);
+                });
+            }
+        );
+    }
+
     // resolves to a hash of perm-name => boolean value indicating
     // wether the user has the permission at org_id.
     service.has_perms_for_org = function(org_id) {
@@ -251,6 +295,11 @@ angular.module('egCoreMod')
                     });
                 });
             });
+
+            egLovefield.setListInOfflineCache('asv', service.surveys)
+            egLovefield.setListInOfflineCache('asvq', service.survey_questions)
+            egLovefield.setListInOfflineCache('asva', service.survey_answers)
+
         });
     }
 
@@ -270,6 +319,7 @@ angular.module('egCoreMod')
                 );
             });
             service.stat_cats = cats;
+            return egLovefield.setStatCatsCache(cats);
         });
     };
 
@@ -284,6 +334,7 @@ angular.module('egCoreMod')
             'ui.patron.registration.require_address',
             'circ.holds.behind_desk_pickup_supported',
             'circ.patron_edit.clone.copy_address',
+            'circ.privacy_waiver',
             'ui.patron.edit.au.prefix.require',
             'ui.patron.edit.au.prefix.show',
             'ui.patron.edit.au.prefix.suggest',
@@ -298,9 +349,11 @@ angular.module('egCoreMod')
             'ui.patron.edit.au.dob.show',
             'ui.patron.edit.au.dob.suggest',
             'ui.patron.edit.au.dob.calendar',
+            'ui.patron.edit.au.dob.example',
             '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.require',
             'ui.patron.edit.au.ident_value.suggest',
             'ui.patron.edit.au.ident_value2.show',
             'ui.patron.edit.au.ident_value2.suggest',
@@ -341,6 +394,9 @@ angular.module('egCoreMod')
             'ui.patron.edit.aua.post_code.regex',
             'ui.patron.edit.aua.post_code.example',
             'ui.patron.edit.aua.county.require',
+            'ui.patron.edit.au.guardian.show',
+            'ui.patron.edit.au.guardian.suggest',
+            'ui.patron.edit.guardian_required_for_juv',
             'format.date',
             'ui.patron.edit.default_suggested',
             'opac.barcode_regex',
@@ -348,9 +404,15 @@ angular.module('egCoreMod')
             'sms.enable',
             'ui.patron.edit.aua.state.require',
             'ui.patron.edit.aua.state.suggest',
-            'ui.patron.edit.aua.state.show'
+            'ui.patron.edit.aua.state.show',
+            'ui.admin.work_log.max_entries',
+            'ui.admin.patron_log.max_entries'
         ]).then(function(settings) {
             service.org_settings = settings;
+            if (egCore && egCore.env && !egCore.env.aous) {
+                egCore.env.aous = settings;
+                console.log('setting egCore.env.aous');
+            }
             return service.process_org_settings(settings);
         });
     };
@@ -358,7 +420,7 @@ angular.module('egCoreMod')
     // some org settings require the retrieval of additional data
     service.process_org_settings = function(settings) {
 
-        var promises = [];
+        var promises = [egLovefield.setSettingsCache(settings)];
 
         if (settings['sms.enable']) {
             // fetch SMS carriers
@@ -427,18 +489,74 @@ angular.module('egCoreMod')
         }
     }
 
+    service.searchPermGroupEntries = function(org) {
+        return egCore.pcrud.search('pgtde', {org: org, parent: null},
+            {flesh: -1, flesh_fields: {pgtde: ['grp', 'children']}}, {atomic: true}
+        ).then(function(treeArray) {
+            if (!treeArray.length && egCore.org.get(org).parent_ou()) {
+                return service.searchPermGroupEntries(egCore.org.get(org).parent_ou());
+            }
+            return treeArray;
+        });
+    }
+
+    service.get_perm_group_entries = function() {
+        if (egCore.env.pgtde) {
+            service.profile_entries = egCore.env.pgtde.list;
+            return service.set_edit_profile_entries();
+        } else {
+            return service.searchPermGroupEntries(egCore.auth.user().ws_ou()).then(function(treeArray) {
+                function compare(a,b) {
+                  if (a.position() > b.position())
+                    return -1;
+                  if (a.position() < b.position())
+                    return 1;
+                  return 0;
+                }
+
+                var list = [];
+                function squash(node) {
+                    node.children().sort(compare);
+                    list.push(node);
+                    angular.forEach(node.children(), squash);
+                }
+
+                angular.forEach(treeArray, squash);
+                var blob = egCore.env.absorbList(list, 'pgtde');
+                blob.tree = treeArray;
+
+                service.profile_entries = egCore.env.pgtde.list;
+                return service.set_edit_profile_entries();
+            });
+        }
+    }
+
     service.get_field_doc = function() {
+        var to_cache = [];
         return egCore.pcrud.search('fdoc', {
             fm_class: ['au', 'ac', 'aua', 'actsc', 'asv', 'asvq', 'asva']})
-        .then(null, null, function(doc) {
-            if (!service.field_doc[doc.fm_class()]) {
-                service.field_doc[doc.fm_class()] = {};
+        .then(
+            function () {
+                return egLovefield.setListInOfflineCache('fdoc', to_cache)
+            },
+            null,
+            function(doc) {
+                if (!service.field_doc[doc.fm_class()]) {
+                    service.field_doc[doc.fm_class()] = {};
+                }
+                service.field_doc[doc.fm_class()][doc.field()] = doc;
+                to_cache.push(doc);
             }
-            service.field_doc[doc.fm_class()][doc.field()] = doc;
-        });
+        );
+
     };
 
-    service.get_user_settings = function() {
+    service.get_user_setting_types = function() {
+
+        // No need to re-fetch the common setting types.
+        if (Object.keys(service.user_setting_types).length) 
+            return $q.when();
+
         var org_ids = egCore.org.ancestors(egCore.auth.user().ws_ou(), true);
 
         var static_types = [
@@ -465,12 +583,23 @@ angular.module('egCoreMod')
             ]
         }, {}, {atomic : true}).then(function(setting_types) {
 
+            egCore.env.absorbList(setting_types, 'cust'); // why not...
+
             angular.forEach(setting_types, function(stype) {
                 service.user_setting_types[stype.name()] = stype;
                 if (static_types.indexOf(stype.name()) == -1) {
                     service.opt_in_setting_types[stype.name()] = stype;
                 }
             });
+        });
+    };
+
+    service.get_user_settings = function() {
+
+        return service.get_user_setting_types()
+        .then(function() {
+
+            var setting_types = Object.values(service.user_setting_types);
 
             if (service.patron_id) {
                 // retrieve applied values for the current user 
@@ -488,13 +617,20 @@ 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.user_settings[setting.name()] = 
-                            setting.reg_default();
+                        var val = stype.reg_default();
+                        if (stype.datatype() == 'bool') {
+                            // A boolean user setting type whose default 
+                            // value starts with t/T is considered 'true',
+                            // false otherwise.
+                            val = Boolean((val+'').match(/^t/i));
+                        }
+                        service.user_settings[stype.name()] = val;
                     }
                 });
             }
@@ -524,6 +660,8 @@ angular.module('egCoreMod')
 
         console.log('Dupe search called with "'+ type +'" and value '+ value);
 
+        if (type.match(/phone/)) type = 'phone'; // day_phone, etc.
+
         switch (type) {
 
             case 'name':
@@ -575,10 +713,10 @@ angular.module('egCoreMod')
     service.init_patron = function(current) {
 
         if (!current)
-            return service.init_new_patron();
+            return $q.when(service.init_new_patron());
 
         service.patron = current;
-        return service.init_existing_patron(current)
+        return $q.when(service.init_existing_patron(current));
     }
 
     service.ingest_address = function(patron, addr) {
@@ -588,6 +726,14 @@ angular.module('egCoreMod')
             addr.id == patron.mailing_address.id);
         addr._is_billing = (patron.billing_address && 
             addr.id == patron.billing_address.id);
+        addr.pending = addr.pending === 't';
+    }
+
+    service.ingest_waiver_entry = function(patron, waiver_entry) {
+        waiver_entry.place_holds = waiver_entry.place_holds == 't';
+        waiver_entry.pickup_holds = waiver_entry.pickup_holds == 't';
+        waiver_entry.view_history = waiver_entry.view_history == 't';
+        waiver_entry.checkout_items = waiver_entry.checkout_items == 't';
     }
 
     /*
@@ -610,6 +756,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.ident_type2 = current.ident_type2();
         patron.groups = current.groups(); // pre-hash
 
         angular.forEach(
@@ -621,13 +768,24 @@ angular.module('egCoreMod')
             card.active = card.active == 't';
             if (card.id == patron.card.id) {
                 patron.card = card;
-                card._primary = 'on';
+                card._primary = true;
             }
         });
 
         angular.forEach(patron.addresses, 
             function(addr) { service.ingest_address(patron, addr) });
 
+        // Link replaced address to its pending address.
+        angular.forEach(patron.addresses, function(addr) {
+            if (addr.replaces) {
+                addr._replaces = patron.addresses.filter(
+                    function(a) {return a.id == addr.replaces})[0];
+            }
+        });
+
+        angular.forEach(patron.waiver_entries,
+            function(waiver_entry) { service.ingest_waiver_entry(patron, waiver_entry) });
+
         service.get_linked_addr_users(patron.addresses);
 
         // Remove stat cat entries that link to out-of-scope stat
@@ -649,6 +807,7 @@ angular.module('egCoreMod')
             service.stat_cat_entry_maps[map.stat_cat.id] = map.stat_cat_entry;
         });
 
+        service.patron = patron;
         return patron;
     }
 
@@ -668,7 +827,7 @@ angular.module('egCoreMod')
             id : service.virt_id--,
             isnew : true,
             active : true,
-            _primary : 'on'
+            _primary : true
         };
 
         var user = {
@@ -678,6 +837,7 @@ angular.module('egCoreMod')
             cards : [card],
             home_ou : egCore.org.get(egCore.auth.user().ws_ou()),
             stat_cat_entries : [],
+            waiver_entries : [],
             groups : [],
             addresses : [addr]
         };
@@ -701,11 +861,7 @@ angular.module('egCoreMod')
     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;
+        return new Date(parts[0], parts[1] - 1, parts[2])
     }
 
     service.copy_stage_data = function(user) {
@@ -725,6 +881,8 @@ angular.module('egCoreMod')
         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];
+        if (user.ident_type2)
+            user.ident_type2 = egCore.env.cit.map[user.ident_type2];
         user.dob = service.parse_dob(user.dob);
 
         // Clear the usrname if it looks like a UUID
@@ -745,6 +903,7 @@ angular.module('egCoreMod')
                 usr : user.id,
                 isnew : true,
                 valid : true,
+                address_type : egCore.strings.REG_ADDR_TYPE,
                 _is_mailing : cls == 'stgma',
                 _is_billing : cls == 'stgba'
             };
@@ -778,13 +937,17 @@ angular.module('egCoreMod')
                 barcode : cuser.cards[0].barcode(),
                 isnew : true,
                 active : true,
-                _primary : 'on'
+                _primary : true
             };
 
             user.cards.push(user.card);
             if (user.usrname == '') 
                 user.usrname = card.barcode;
         }
+
+        angular.forEach(cuser.settings, function(setting) {
+            service.user_settings[setting.setting()] = Boolean(setting.value());
+        });
     }
 
     // copy select values from the cloned user to the new user.
@@ -935,6 +1098,7 @@ angular.module('egCoreMod')
             patron.addresses().push(addr);
             addr.valid(addr.valid() ? 't' : 'f');
             addr.within_city_limits(addr.within_city_limits() ? 't' : 'f');
+            addr.pending(addr.pending() ? 't' : 'f');
             if (addr_hash._is_mailing) patron.mailing_address(addr);
             if (addr_hash._is_billing) patron.billing_address(addr);
         });
@@ -988,6 +1152,15 @@ angular.module('egCoreMod')
             patron.stat_cat_entries().push(newmap);
         });
 
+        var waiver_hashes = patron.waiver_entries();
+        patron.waiver_entries([]);
+        angular.forEach(waiver_hashes, function(waiver_hash) {
+            if (!waiver_hash.isnew && !waiver_hash.isdeleted)
+                waiver_hash.ischanged = true;
+            var waiver_entry = egCore.idl.fromHash('aupw', waiver_hash);
+            patron.waiver_entries().push(waiver_entry);
+        });
+
         if (!patron.isnew()) patron.ischanged(true);
 
         return egCore.net.request(
@@ -1002,24 +1175,20 @@ angular.module('egCoreMod')
             'open-ils.actor',
             'open-ils.actor.user.stage.delete',
             egCore.auth.token(),
-            service.stage_user.row_id()
+            service.stage_user.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.
 
         var settings = {};
         if (service.patron_id) {
-            // only update modified settings for existing patrons
-            angular.forEach(user_settings, function(val, key) {
-                if (val !== service.user_settings[key])
-                    settings[key] = val;
-            });
+            // Update all user editor setting values for existing 
+            // users regardless of whether a value changed.
+            settings = user_settings;
 
         } else {
-            // all non-null setting values are updated for new patrons
+            // Create settings for all non-null setting values for new patrons.
             angular.forEach(user_settings, function(val, key) {
                 if (val !== null) settings[key] = val;
             });
@@ -1045,9 +1214,9 @@ angular.module('egCoreMod')
                 new RegExp(service.org_settings['opac.username_regex']);
         }
 
-        if (service.org_settings['opac.barcode_regex']) {
+        if (service.org_settings['ui.patron.edit.ac.barcode.regex']) {
             patterns.ac.barcode = 
-                new RegExp(service.org_settings['opac.barcode_regex']);
+                new RegExp(service.org_settings['ui.patron.edit.ac.barcode.regex']);
         }
 
         if (service.org_settings['global.password_regex']) {
@@ -1076,13 +1245,18 @@ angular.module('egCoreMod')
     }
 
     return service;
-}]);
-
+}])
 
-function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore, 
-    patronSvc, patronRegSvc, egUnloadPrompt, egAlertDialog) {
+.controller('PatronRegCtrl',
+       ['$scope','$routeParams','$q','$uibModal','$window','egCore',
+        'patronSvc','patronRegSvc','egUnloadPrompt','egAlertDialog',
+        'egWorkLog', '$timeout',
+function($scope , $routeParams , $q , $uibModal , $window , egCore ,
+         patronSvc , patronRegSvc , egUnloadPrompt, egAlertDialog ,
+         egWorkLog, $timeout) {
 
     $scope.page_data_loaded = false;
+    $scope.hold_notify_type = { phone : null, email : null, sms : null };
     $scope.clone_id = patronRegSvc.clone_id = $routeParams.clone_id;
     $scope.stage_username = 
         patronRegSvc.stage_username = $routeParams.stage_username;
@@ -1092,12 +1266,15 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
     // 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.address_alerts = [];
     $scope.dupe_counts = {};
 
     // map of perm name to true/false for perms the logged in user
     // has at the currently selected patron home org unit.
     $scope.perms = {};
 
+    $scope.name_tab = 'primary';
+
     if (!$scope.edit_passthru) {
         // in edit more, scope.edit_passthru is delivered to us by
         // the enclosing controller.  In register mode, there is 
@@ -1115,8 +1292,9 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
             // passsword may originate from staged user.
             $scope.generate_password();
         }
-        $scope.hold_notify_phone = true;
-        $scope.hold_notify_email = true;
+        $scope.hold_notify_type.phone = true;
+        $scope.hold_notify_type.email = true;
+        $scope.hold_notify_type.sms = false;
 
         // staged users may be loaded w/ a profile.
         $scope.set_expire_date();
@@ -1161,16 +1339,17 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         $scope.initTab ? // initTab comes from patron app
             $scope.initTab('edit', $routeParams.id) : $q.when(),
 
-        patronRegSvc.init()
+        patronRegSvc.init(),
 
-    ]).then(function() {
+    ]).then(function(){ return patronRegSvc.init_patron(patronSvc ? patronSvc.current : patronRegSvc.patron ) })
+      .then(function(patron) {
         // 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);
+        var prs = patronRegSvc;
+        $scope.patron = patron;
         $scope.field_doc = prs.field_doc;
         $scope.edit_profiles = prs.edit_profiles;
+        $scope.edit_profile_entries = prs.edit_profile_entries;
         $scope.ident_types = prs.ident_types;
         $scope.net_access_levels = prs.net_access_levels;
         $scope.user_setting_types = prs.user_setting_types;
@@ -1185,28 +1364,65 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         $scope.stage_user_requestor = prs.stage_user_requestor;
 
         $scope.user_settings = prs.user_settings;
-        // clone the user settings back into the patronRegSvc so
-        // we have a copy of the original state of the settings.
         prs.user_settings = {};
-        angular.forEach($scope.user_settings, function(val, key) {
-            prs.user_settings[key] = val;
-        });
+
+        // If a default pickup lib is applied to the patron, apply it 
+        // to the UI at page load time.  Otherwise, leave the value unset.
+        if ($scope.user_settings['opac.default_pickup_location']) {
+            $scope.patron._pickup_lib = egCore.org.get(
+                $scope.user_settings['opac.default_pickup_location']);
+        }
 
         extract_hold_notify();
+        if ($scope.patron.isnew)
+            set_new_patron_defaults(prs);
+
         $scope.handle_home_org_changed();
 
         if ($scope.org_settings['ui.patron.edit.default_suggested'])
             $scope.edit_passthru.vis_level = 1;
 
-        if ($scope.patron.isnew) 
-            set_new_patron_defaults(prs);
+        // Stat cats are fetched from open-ils.storage, where 't'==1
+        $scope.hasRequiredStatCat = prs.stat_cats.filter(
+                function(cat) {return cat.required() == 1} ).length > 0;
 
         $scope.page_data_loaded = true;
 
         prs.set_field_patterns(field_patterns);
         apply_username_regex();
+
+        add_date_watchers();
+
+        if ($scope.org_settings['ui.patron.edit.guardian_required_for_juv']) {
+            add_juv_watcher();
+        }
     });
 
+    function add_date_watchers() {
+
+        $scope.$watch('patron.dob', function(newVal, oldVal) {
+            // Even though this runs after page data load, there
+            // are still times when it fires unnecessarily.
+            if (newVal === oldVal) return;
+
+            console.debug('dob change: ' + newVal + ' : ' + oldVal);
+            maintain_juvenile_flag();
+        });
+
+        // No need to watch expire_date
+    }
+
+    function add_juv_watcher() {
+        $scope.$watch('patron.juvenile', function(newVal, oldVal) {
+            if (newVal === oldVal) return;
+            if (newVal) {
+                field_visibility['au.guardian'] = 3; // required
+            } else {
+                // Value will be reassessed by show_field()
+                delete field_visibility['au.guardian'];
+            }
+        });
+    }
 
     // update the currently displayed field documentation
     $scope.set_selected_field_doc = function(cls, field) {
@@ -1220,6 +1436,13 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         return d;
     }
 
+    // returns the tree depth of the selected profile group tree node.
+    $scope.pgtde_depth = function(entry) {
+        var d = 0;
+        while (entry = egCore.env.pgtde.map[entry.parent()]) d++;
+        return d;
+    }
+
     // IDL fields used for labels in the UI.
     $scope.idl_fields = {
         au  : egCore.idl.classes.au.field_map,
@@ -1238,7 +1461,10 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         'au.passwd' :  3,
         'au.first_given_name' : 3,
         'au.family_name' : 3,
+        'au.pref_first_given_name' : 2,
+        'au.pref_family_name' : 2,
         'au.ident_type' : 3,
+        'au.ident_type2' : 2,
         'au.home_ou' : 3,
         'au.profile' : 3,
         'au.expire_date' : 3,
@@ -1254,7 +1480,8 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         'aua.valid' : 2,
         'aua.within_city_limits' : 2,
         'stat_cats' : 1,
-        'surveys' : 1
+        'surveys' : 1,
+        'au.name_keywords': 1
     }; 
 
     // Returns true if the selected field should be visible
@@ -1268,12 +1495,26 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         if (field_visibility[field_key] == undefined) {
             // compile and cache the visibility for the selected field
 
-            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';
+            // The preferred name fields use the primary name field settings
+            var org_key = field_key;
+            var alt_name = false;
+            if (field_key.match(/^au.alt_/)) {
+                alt_name = true;
+                org_key = field_key.slice(7);
+            }
+
+            var req_set = 'ui.patron.edit.' + org_key + '.require';
+            var sho_set = 'ui.patron.edit.' + org_key + '.show';
+            var sug_set = 'ui.patron.edit.' + org_key + '.suggest';
 
             if ($scope.org_settings[req_set]) {
-                field_visibility[field_key] = 3;
+                if (alt_name) {
+                    // Avoid requiring alt name fields when primary 
+                    // name fields are required.
+                    field_visibility[field_key] = 2;
+                } else {
+                    field_visibility[field_key] = 3;
+                }
 
             } else if ($scope.org_settings[sho_set]) {
                 field_visibility[field_key] = 2;
@@ -1318,10 +1559,13 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         var now_epoch = new Date().getTime();
         $scope.patron.expire_date = new Date(
             now_epoch + (seconds * 1000 /* milliseconds */))
+        $scope.field_modified();
     }
 
     // grp is the pgt object
     $scope.set_profile = function(grp) {
+        // If we can't save because of group perms or create/update perms
+        if ($scope.edit_passthru.hide_save_actions()) return;
         $scope.patron.profile = grp;
         $scope.set_expire_date();
         $scope.field_modified();
@@ -1373,6 +1617,37 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         $scope.patron.addresses = addresses;
     } 
 
+    $scope.approve_pending_address = function(addr) {
+
+        egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.pending_address.approve',
+            egCore.auth.token(), addr.id
+        ).then(function(replaced_id) {
+            var evt = egCore.evt.parse(replaced_id);
+            if (evt) { alert(evt); return; }
+
+            // Remove the pending address and the replaced address
+            // from the local list of patron addresses.
+            var addresses = [];
+            angular.forEach($scope.patron.addresses, function(a) {
+                if (a.id != addr.id && a.id != replaced_id) {
+                    addresses.push(a);
+                }
+            });
+            $scope.patron.addresses = addresses;
+
+            // Fetch a fresh copy of the modified address from the server.
+            // and add it back to the list.
+            egCore.pcrud.retrieve('aua', replaced_id, {}, {authoritative: true})
+            .then(null, null, function(new_addr) {
+                new_addr = egCore.idl.toHash(new_addr);
+                patronRegSvc.ingest_address($scope.patron, new_addr);
+                $scope.patron.addresses.push(new_addr);
+            });
+        });
+    }
+
     $scope.post_code_changed = function(addr) { 
         egCore.net.request(
             'open-ils.search', 'open-ils.search.zip', addr.post_code)
@@ -1385,6 +1660,24 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         });
     }
 
+    $scope.new_waiver_entry = function() {
+        var waiver = egCore.idl.toHash(new egCore.idl.aupw());
+        patronRegSvc.ingest_waiver_entry($scope.patron, waiver);
+        waiver.id = patronRegSvc.virt_id--;
+        waiver.isnew = true;
+        $scope.patron.waiver_entries.push(waiver);
+    }
+
+    deleted_waiver_entries = [];
+    $scope.delete_waiver_entry = function(waiver_entry) {
+        if (waiver_entry.id > 0) {
+            waiver_entry.isdeleted = true;
+            deleted_waiver_entries.push(waiver_entry);
+        }
+        var index = $scope.patron.waiver_entries.indexOf(waiver_entry);
+        $scope.patron.waiver_entries.splice(index, 1);
+    }
+
     $scope.replace_card = function() {
         $scope.patron.card.active = false;
         $scope.patron.card.ischanged = true;
@@ -1396,6 +1689,11 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         new_card.active = true;
         new_card._primary = 'on';
         $scope.patron.card = new_card;
+
+        // Remove any previous attempts to replace the card, since they
+        // may be incomplete or created by accident.
+        $scope.patron.cards =
+            $scope.patron.cards.filter(function(c) {return !c.isnew})
         $scope.patron.cards.push(new_card);
     }
 
@@ -1426,16 +1724,22 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
     }
 
     $scope.cards_dialog = function() {
-        $modal.open({
+        $uibModal.open({
             templateUrl: './circ/patron/t_patron_cards_dialog',
+            backdrop: 'static',
             controller: 
-                   ['$scope','$modalInstance','cards', 'perms',
-            function($scope , $modalInstance , cards, perms) {
+                   ['$scope','$uibModalInstance','cards','perms','patron',
+            function($scope , $uibModalInstance , cards , perms , patron) {
                 // scope here is the modal-level scope
-                $scope.args = {cards : cards};
+                $scope.args = {cards : cards, primary_barcode : null};
+                angular.forEach(cards, function(card) {
+                    if (card.id == patron.card.id) {
+                        $scope.args.primary_barcode = card.id;
+                    }
+                });
                 $scope.perms = perms;
-                $scope.ok = function() { $modalInstance.close($scope.args) }
-                $scope.cancel = function () { $modalInstance.dismiss() }
+                $scope.ok = function() { $uibModalInstance.close($scope.args) }
+                $scope.cancel = function () { $uibModalInstance.dismiss() }
             }],
             resolve : {
                 cards : function() {
@@ -1444,15 +1748,20 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
                 },
                 perms : function() {
                     return $scope.perms;
+                },
+                patron : function() {
+                    return $scope.patron;
                 }
             }
         }).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) {
+                    if (card.id == args.primary_barcode) {
                         $scope.patron.card = card;
+                        card._primary = true;
+                    } else {
+                        card._primary = false;
                     }
                 });
             }
@@ -1483,30 +1792,28 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
     // Translate hold notify preferences from the form/scope back into a 
     // single user setting value for opac.hold_notify.
     function compress_hold_notify() {
-        var hold_notify = '';
-        var splitter = '';
-        if ($scope.hold_notify_phone) {
-            hold_notify = 'phone';
-            splitter = ':';
+        var hold_notify_methods = [];
+        if ($scope.hold_notify_type.phone) {
+            hold_notify_methods.push('phone');
         }
-        if ($scope.hold_notify_email) {
-            hold_notify = splitter + 'email';
-            splitter = ':';
+        if ($scope.hold_notify_type.email) {
+            hold_notify_methods.push('email');
         }
-        if ($scope.hold_notify_sms) {
-            hold_notify = splitter + 'sms';
-            splitter = ':';
+        if ($scope.hold_notify_type.sms) {
+            hold_notify_methods.push('sms');
         }
-        $scope.user_settings['opac.hold_notify'] = hold_notify;
+
+        $scope.user_settings['opac.hold_notify'] = hold_notify_methods.join(':');
     }
 
     // dialog for selecting additional permission groups
     $scope.secondary_groups_dialog = function() {
-        $modal.open({
+        $uibModal.open({
             templateUrl: './circ/patron/t_patron_groups_dialog',
+            backdrop: 'static',
             controller: 
-                   ['$scope','$modalInstance','linked_groups','pgt_depth',
-            function($scope , $modalInstance , linked_groups , pgt_depth) {
+                   ['$scope','$uibModalInstance','linked_groups','pgt_depth',
+            function($scope , $uibModalInstance , linked_groups , pgt_depth) {
 
                 $scope.pgt_depth = pgt_depth;
                 $scope.args = {
@@ -1533,8 +1840,8 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
                     $event.preventDefault(); // avoid close
                 }
 
-                $scope.ok = function() { $modalInstance.close($scope.args) }
-                $scope.cancel = function () { $modalInstance.dismiss() }
+                $scope.ok = function() { $uibModalInstance.close($scope.args) }
+                $scope.cancel = function () { $uibModalInstance.dismiss() }
             }],
             resolve : {
                 linked_groups : function() { return $scope.patron.groups },
@@ -1562,28 +1869,53 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
     }
 
     function extract_hold_notify() {
-        notify = $scope.user_settings['opac.hold_notify'];
+        var 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.hold_notify_type.phone = Boolean(notify.match(/phone/));
+        $scope.hold_notify_type.email = Boolean(notify.match(/email/));
+        $scope.hold_notify_type.sms = Boolean(notify.match(/sms/));
     }
 
     $scope.invalidate_field = function(field) {
         patronRegSvc.invalidate_field($scope.patron, field);
     }
 
+    address_alert = function(addr) {
+        var args = {
+            street1: addr.street1,
+            street2: addr.street2,
+            city: addr.city,
+            state: addr.state,
+            county: addr.county,
+            country: addr.country,
+            post_code: addr.post_code,
+            mailing_address: addr._is_mailing,
+            billing_address: addr._is_billing
+        }
+
+        egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.address_alert.test',
+            egCore.auth.token(), egCore.auth.user().ws_ou(), args
+            ).then(function(res) {
+                $scope.address_alerts = res;
+        });
+    }
 
     $scope.dupe_value_changed = function(type, value) {
+        if (!$scope.dupe_search_encoded)
+            $scope.dupe_search_encoded = {};
+
         $scope.dupe_counts[type] = 0;
+
         patronRegSvc.dupe_patron_search($scope.patron, type, value)
         .then(function(res) {
             $scope.dupe_counts[type] = res.count;
             if (res.count) {
-                $scope.dupe_search_encoded = 
+                $scope.dupe_search_encoded[type] = 
                     encodeURIComponent(js2JSON(res.search));
             } else {
-                $scope.dupe_search_encoded = '';
+                $scope.dupe_search_encoded[type] = '';
             }
         });
     }
@@ -1595,6 +1927,11 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         });
     }
 
+    $scope.handle_pulib_changed = function(org) {
+        if (!$scope.user_settings) return; // still rendering
+        $scope.user_settings['opac.default_pickup_location'] = org.id();
+    }
+
     // This is called with every character typed in a form field,
     // since that's the only way to gaurantee something has changed.
     // See handle_field_changed for ng-change vs. ng-blur.
@@ -1606,6 +1943,12 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         egUnloadPrompt.attach($scope);
     }
 
+    // also monitor when form is changed *by the user*, as using
+    // an ng-change handler doesn't work with eg-date-input
+    $scope.$watch('reg_form.$pristine', function(newVal, oldVal) {
+        if (!newVal) egUnloadPrompt.attach($scope);
+    });
+
     // username regex (if present) must be removed any time
     // the username matches the barcode to avoid firing the
     // invalid field handlers.
@@ -1636,18 +1979,18 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         var cls = obj.classname; // set by egIdl
         var value = obj[field_name];
 
-        console.log('changing field ' + field_name + ' to ' + value);
+        console.debug('changing field ' + field_name + ' to ' + value);
 
         switch (field_name) {
             case 'day_phone' : 
                 if ($scope.patron.day_phone && 
                     $scope.patron.isnew && 
                     $scope.org_settings['patron.password.use_phone']) {
-                    $scope.patron.passwd = phone.substr(-4);
+                    $scope.patron.passwd = $scope.patron.day_phone.substr(-4);
                 }
             case 'evening_phone' : 
             case 'other_phone' : 
-                $scope.dupe_value_changed('phone', value);
+                $scope.dupe_value_changed(field_name, value);
                 break;
 
             case 'ident_value':
@@ -1669,6 +2012,7 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
             case 'city':
                 // dupe search on address wants the address object as the value.
                 $scope.dupe_value_changed('address', obj);
+                address_alert(obj);
                 break;
 
             case 'post_code':
@@ -1685,10 +2029,6 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
                 $scope.barcode_changed(value);
                 apply_username_regex();
                 break;
-
-            case 'dob':
-                maintain_juvenile_flag();
-                break;
         }
     }
 
@@ -1719,8 +2059,26 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         );
     }
 
+    // returns true (disable) for orgs that cannot have vols (for holds pickup)
+    $scope.disable_pulib = function(org_id) {
+        if (!org_id) return;
+        return !egCore.org.CanHaveVolumes(org_id);
+    }
+
     // Returns true if the Save and Save & Clone buttons should be disabled.
     $scope.edit_passthru.hide_save_actions = function() {
+        if ($scope.patron.id
+            && $scope.patron.id == egCore.auth.user().id()
+        ) return true;
+
+        if ( $scope.patron.profile
+             && patronRegSvc
+                .edit_profiles
+                .filter(function(p) {
+                    return $scope.patron.profile.id() == p.id();
+                }).length == 0
+        ) return true;
+
         return $scope.patron.isnew ?
             !$scope.perms.CREATE_USER : 
             !$scope.perms.UPDATE_USER;
@@ -1757,6 +2115,10 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
         $scope.patron.addresses = 
             $scope.patron.addresses.concat(deleted_addresses);
         
+        // ditto for waiver entries
+        $scope.patron.waiver_entries = 
+            $scope.patron.waiver_entries.concat(deleted_waiver_entries);
+
         compress_hold_notify();
 
         var updated_user;
@@ -1768,6 +2130,14 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
                 return patronRegSvc.save_user_settings(
                     new_user, $scope.user_settings); 
             } else {
+                var evt = egCore.evt.parse(new_user);
+
+                if (evt && evt.textcode == 'XACT_COLLISION') {
+                    return egAlertDialog.open(
+                        egCore.strings.PATRON_EDIT_COLLISION).result;
+                }
+
+                // debug only -- should not get here.
                 alert('Patron update failed. \n\n' + js2JSON(new_user));
             }
 
@@ -1793,6 +2163,17 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
 
         }).then(function() {
 
+            if (updated_user) {
+                egWorkLog.record(
+                    $scope.patron.isnew
+                    ? egCore.strings.EG_WORK_LOG_REGISTERED_PATRON
+                    : egCore.strings.EG_WORK_LOG_EDITED_PATRON, {
+                        'action' : $scope.patron.isnew ? 'registered_patron' : 'edited_patron',
+                        'patron_id' : updated_user.id()
+                    }
+                );
+            }
+
             // reloading the page means potentially losing some information
             // (e.g. last patron search), but is the only way to ensure all
             // components are properly updated to reflect the modified patron.
@@ -1806,16 +2187,33 @@ function PatronRegCtrl($scope, $routeParams, $q, $modal, $window, egCore,
                     + updated_user.id();
                 $window.open(url, '_blank').focus();
 
+            } else if ($window.location.href.indexOf('stage') > -1 ){
+                // we're here after deleting a self-reg staged user.
+                // Just close tab, since refresh won't find staged user
+                $timeout(function(){
+                    if (typeof BroadcastChannel != 'undefined') {
+                        var bChannel = new BroadcastChannel("eg.pending_usr.update");
+                        bChannel.postMessage({
+                            usr: egCore.idl.toHash(updated_user)
+                        });
+                    }
+
+                    $window.close();
+                });
             } else {
                 // reload the current page
                 $window.location.href = location.href;
             }
         });
     }
-}
 
-// 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', 'egUnloadPrompt', 'egAlertDialog'];
+    $scope.edit_passthru.print = function() {
+        var print_data = {patron : $scope.patron}
 
+        return egCore.print.print({
+            context : 'default',
+            template : 'patron_data',
+            scope : print_data
+        });
+    }
+}])