LP#1452950 angularize patron registration phase I
authorBill Erickson <berickxx@gmail.com>
Mon, 4 May 2015 01:19:51 +0000 (21:19 -0400)
committerGalen Charlton <gmc@esilibrary.com>
Thu, 25 Feb 2016 22:31:54 +0000 (17:31 -0500)
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@gmail.com>
Signed-off-by: Galen Charlton <gmc@esilibrary.com>
Open-ILS/src/templates/staff/circ/patron/index.tt2
Open-ILS/src/templates/staff/circ/patron/register.tt2
Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
Open-ILS/src/templates/staff/css/circ.css.tt2
Open-ILS/src/templates/staff/css/style.css.tt2
Open-ILS/web/js/ui/default/staff/circ/patron/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/circ/patron/register.js

index bb0d2f8..3234b95 100644 (file)
@@ -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? -->
 
index 4a3c9ce..a0b2af4 100644 (file)
@@ -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 %]
 
index 3f85366..17dde5e 100644 (file)
@@ -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>
+
+
 
index 8d6c139..bf9d834 100644 (file)
@@ -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 
index 0c21913..5c367b8 100644 (file)
@@ -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; }
index d5fb95d..46aab7c 100644 (file)
@@ -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 (file)
index 0000000..b940514
--- /dev/null
@@ -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 
index 13b4a41..49306f0 100644 (file)
@@ -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;
-}])