webstaff: Serials Admin menu and Serial Templates collab/phasefx/webstaff_serial_templates
authorJason Etheridge <jason@esilibrary.com>
Fri, 28 Apr 2017 19:38:40 +0000 (15:38 -0400)
committerJason Etheridge <jason@esilibrary.com>
Fri, 5 May 2017 19:07:06 +0000 (15:07 -0400)
Signed-off-by: Jason Etheridge <jason@esilibrary.com>
Open-ILS/src/templates/staff/admin/serials/index.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/admin/serials/t_attr_edit.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/admin/serials/t_splash.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/admin/serials/t_template_list.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/admin/serials/t_templates.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/navbar.tt2
Open-ILS/web/js/ui/default/staff/admin/serials/app.js [new file with mode: 0644]

diff --git a/Open-ILS/src/templates/staff/admin/serials/index.tt2 b/Open-ILS/src/templates/staff/admin/serials/index.tt2
new file mode 100644 (file)
index 0000000..2a54931
--- /dev/null
@@ -0,0 +1,33 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Serials Administration"); 
+  ctx.page_app = "egSerialsAdmin";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/file.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/admin/serials/app.js"></script>
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+    s.SERIALS_TEMPLATE_SUCCESS_SAVE = "[% l('Saved serial template') %]";
+    s.SERIALS_TEMPLATE_SUCCESS_DELETE = "[% l('Deleted serial template') %]";
+    s.SERIALS_TEMPLATE_FAIL_SAVE = "[% l('Failed to save serial template') %]";
+    s.SERIALS_TEMPLATE_FAIL_DELETE = "[% l('Failed to delete serial template') %]";
+    s.LOAN_DURATION_SHORT = "[% l('Short') %]";
+    s.LOAN_DURATION_NORMAL = "[% l('Normal') %]";
+    s.LOAN_DURATION_EXTENDED = "[% l('Extended') %]";
+    s.FINE_LEVEL_LOW = "[% l('Low') %]";
+    s.FINE_LEVEL_NORMAL = "[% l('Normal') %]";
+    s.FINE_LEVEL_HIGH = "[% l('High') %]";
+    s.CONFIRM_DIRTY_EXIT = "[% l('There are unsaved changes; close anyway?') %]";
+}]);
+</script>
+[% END %]
+
+<div ng-view></div>
+
+[% END %]
+
+
diff --git a/Open-ILS/src/templates/staff/admin/serials/t_attr_edit.tt2 b/Open-ILS/src/templates/staff/admin/serials/t_attr_edit.tt2
new file mode 100644 (file)
index 0000000..d400ae4
--- /dev/null
@@ -0,0 +1,338 @@
+<style>
+    .app-modal-window .modal-dialog {
+      width: 800px;
+    }
+    .vertical-align {
+        display: flex;
+        align-items: center;
+    }
+</style>
+
+<form role="form">
+<div class="container-fluid">
+    <div class="row bg-info vertical-align">
+        <div class="col-md-3">
+            <h4>[% l('Template Name') %]</h4>
+        </div>
+        <div class="col-md-3">
+            <input type="text" class="form-control" ng-model="working.name"></input>
+        </div>
+<!-- FIXME: remove for now; may be nice to have later
+        <div class="col-md-2">
+            <div class="btn-group pull-right">
+                <span class="btn btn-default btn-file">
+                    [% l('Import') %]
+                    <input type="file" eg-file-reader container="imported_template.data">
+                </span>
+                <label class="btn btn-default"
+                    eg-json-exporter container="hashed_template"
+                    default-file-name="'[% l('exported_serials_template.json') %]'">
+                    [% l('Export') %]
+                </label>
+            </div>
+        </div>
+-->
+        <div class="col-md-4">
+            <div class="btn-group pull-right">
+                <button class="btn btn-default" ng-click="clearWorking()" type="button">[% l('Clear') %]</button>
+                <button class="btn btn-default" ng-disabled="working.name==''" ng-click="saveTemplate()" type="button">[% l('Save') %]</label>
+                <button class="btn btn-default" ng-click="close_modal()" type="button">[% l('Close') %]</label>
+            </div>
+        </div>
+    </div>
+
+    <div class="row pad-vert"></div>
+
+    <div class="row bg-info">
+        <div class="col-md-4">
+            <b>[% l('Circulate?') %]</b>
+        </div>
+        <div class="col-md-4">
+            <b>[% l('Status') %]</b>
+        </div>
+    </div>
+
+    <div class="row">
+        <div class="col-md-8">
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.circulate !== undefined}">
+                    <div class="row">
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-disabled="!defaults.attributes.circulate" ng-model="working.circulate" value="t"/>
+                                [% l('Yes') %]
+                            </label>
+                        </div>
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-disabled="!defaults.attributes.circulate" ng-model="working.circulate" value="f"/>
+                                [% l('No') %]
+                            </label>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.status !== undefined}">
+                    <select class="form-control"
+                        ng-disabled="!defaults.attributes.status" ng-model="working.status"
+                        ng-options="s.id() as s.name() for s in status_list">
+                    </select>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Circulation Library') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('Reference?') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.circ_lib !== undefined}">
+                    <eg-org-selector
+                        alldisabled="{{!defaults.attributes.circ_lib}}"
+                        selected="working.circ_lib"
+                        noDefault
+                        label="[% l('(Unset)') %]"
+                        disable-test="cant_have_vols"
+                    ></eg-org-selector>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.ref !== undefined}">
+                    <div class="row">
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-disabled="!defaults.attributes.ref" ng-model="working.ref" value="t"/>
+                                [% l('Yes') %]
+                            </label>
+                        </div>
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-disabled="!defaults.attributes.ref" ng-model="working.ref" value="f"/>
+                                [% l('No') %]
+                            </label>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Shelving Location') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('OPAC Visible?') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.location !== undefined}">
+                    <select class="form-control"
+                        ng-disabled="!defaults.attributes.location" ng-model="working.location"
+                        ng-options="l.id() as l.name() for l in location_list"
+                    ></select>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.opac_visible !== undefined}">
+                    <div class="row">
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-disabled="!defaults.attributes.opac_visible" ng-model="working.opac_visible" value="t"/>
+                                [% l('Yes') %]
+                            </label>
+                        </div>
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-disabled="!defaults.attributes.opac_visible" ng-model="working.opac_visible" value="f"/>
+                                [% l('No') %]
+                            </label>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Circulation Modifer') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('Price') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="nullable col-md-6" ng-class="{'bg-success': working.circ_modifier !== undefined}">
+                    <select class="form-control"
+                        ng-disabled="!defaults.attributes.circ_modifier" ng-model="working.circ_modifier"
+                        ng-options="m.code() as m.name() for m in circ_modifier_list"
+                    >
+                        <option value="">[% l('<NONE>') %]</option>
+                    </select>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.price !== undefined}">
+                    <input class="form-control" ng-disabled="!defaults.attributes.price" ng-model="working.price" type="text"/>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Loan Duration') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.loan_duration !== undefined}">
+                    <select class="form-control" ng-disabled="!defaults.attributes.loan_duration" ng-model="working.loan_duration" ng-options="x.v() as x.l() for x in loan_duration_options">
+                    </select>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Circulate as Type') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('Deposit?') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="nullable col-md-6" ng-class="{'bg-success': working.circ_as_type !== undefined}">
+                    <select class="form-control"
+                        ng-disabled="!defaults.attributes.circ_as_type" ng-model="working.circ_as_type"
+                        ng-options="t.code() as t.value() for t in circ_type_list">
+                      <option value="">[% l('<NONE>') %]</option>
+                    </select>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.deposit !== undefined}">
+                    <div class="row">
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-disabled="!defaults.attributes.deposit" ng-model="working.deposit" value="t"/>
+                                [% l('Yes') %]
+                            </label>
+                        </div>
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-disabled="!defaults.attributes.deposit" ng-model="working.deposit" value="f"/>
+                                [% l('No') %]
+                            </label>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Holdable?') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('Deposit Amount') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.holdable !== undefined}">
+                    <div class="row">
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-disabled="!defaults.attributes.holdable" ng-model="working.holdable" value="t"/>
+                                [% l('Yes') %]
+                            </label>
+                        </div>
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-disabled="!defaults.attributes.holdable" ng-model="working.holdable" value="f"/>
+                                [% l('No') %]
+                            </label>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.deposit_amount !== undefined}">
+                    <input class="form-control" ng-disabled="!defaults.attributes.deposit_amount" ng-model="working.deposit_amount" type="text"/>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Age-based Hold Protection') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('Quality') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.age_protect !== undefined}">
+                    <select class="form-control"
+                        ng-disabled="!defaults.attributes.age_protect" ng-model="working.age_protect"
+                        ng-options="a.id() as a.name() for a in age_protect_list"
+                    ></select>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.mint_condition !== undefined}">
+                    <div class="row">
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-disabled="!defaults.attributes.mint_condition" ng-model="working.mint_condition" value="t"/>
+                                [% l('Good') %]
+                            </label>
+                        </div>
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-disabled="!defaults.attributes.mint_condition" ng-model="working.mint_condition" value="f"/>
+                                [% l('Damaged') %]
+                            </label>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Fine Level') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.fine_level !== undefined}">
+                    <select class="form-control" ng-disabled="!defaults.attributes.fine_level" ng-model="working.fine_level" ng-options="x.v() as x.l() for x in fine_level_options">
+                    </select>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Floating') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.floating !== undefined}">
+                    <select class="form-control"
+                        ng-disabled="!defaults.attributes.floating" ng-model="working.floating"
+                        ng-options="a.id() as a.name() for a in floating_list"
+                    ></select>
+                </div>
+            </div>
+        </div>
+
+    </div>
+</div>
+</form>
diff --git a/Open-ILS/src/templates/staff/admin/serials/t_splash.tt2 b/Open-ILS/src/templates/staff/admin/serials/t_splash.tt2
new file mode 100644 (file)
index 0000000..f5060fa
--- /dev/null
@@ -0,0 +1,37 @@
+
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    <span>[% l('Serials Administration') %]</span>
+  </div>
+</div>
+
+<div class="container admin-splash-container">
+
+[%
+    interfaces = [
+     [ l('Serial Templates'), "./admin/serials/templates" ]
+   ];
+
+   USE table(interfaces, cols=3);
+%]
+
+<div class="row">
+    [% FOREACH col = table.cols %]
+        <div class="col-md-4">
+        [% FOREACH item = col %][% IF item.1 %]
+        <div class="row new-entry">
+            <div class="col-md-12">
+                <span class="glyphicon glyphicon-pencil"></span>
+                <a target="_self" href="[% item.1 %]">
+                    [% item.0 %]
+                </a>
+            </div>
+        </div>
+        [% END %]
+    [% END %]
+        </div>
+    [% END %]
+</div>
+
+</div>
+
diff --git a/Open-ILS/src/templates/staff/admin/serials/t_template_list.tt2 b/Open-ILS/src/templates/staff/admin/serials/t_template_list.tt2
new file mode 100644 (file)
index 0000000..14f37ce
--- /dev/null
@@ -0,0 +1,54 @@
+<eg-grid
+  id-field="id"
+  idl-class="act"
+  features="-sort,-multisort"
+  grid-controls="grid_controls"
+  persist-key="serials.copy_templates">
+
+  <eg-grid-menu-item handler="grid_actions.create_template" 
+    label="[% l('Create Template') %]"></eg-grid-menu-item>
+
+  <eg-grid-action handler="grid_actions.edit_template"
+    label="[% l('Edit Template') %]"
+    disabled="need_one_selected"></eg-grid-action>
+
+  <eg-grid-action handler="grid_actions.delete_template"
+    label="[% l('Delete Template') %]"></eg-grid-action>
+
+  <eg-grid-field label="[% l('Template ID') %]" path='id' required></eg-grid-field>
+
+  <eg-grid-field label="[% l('Template Name') %]" path='name'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Create Date') %]"
+    path='create_date'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Creator') %]"
+    path='creator.usrname'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Edit Date') %]"
+    path='edit_date'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Editor') %]"
+    path='editor.usrname'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Owning Library') %]"
+    path='owning_lib.shortname'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Circulating Library') %]"
+    path='circ_lib.shortname' hidden></eg-grid-field>
+
+  <eg-grid-field label="[% l('Status') %]"
+    path='status.name' hidden></eg-grid-field>
+
+  <eg-grid-field label="[% l('Circ Modifier') %]"
+    path='circ_modifier.code' hidden></eg-grid-field>
+
+  <eg-grid-field label="[% l('Location') %]"
+    path='location.name' hidden></eg-grid-field>
+
+  <eg-grid-field label="[% l('Floating') %]"
+    path='floating.name' hidden></eg-grid-field>
+
+  <eg-grid-field path='*' hidden></eg-grid-field>
+</eg-grid>
+
diff --git a/Open-ILS/src/templates/staff/admin/serials/t_templates.tt2 b/Open-ILS/src/templates/staff/admin/serials/t_templates.tt2
new file mode 100644 (file)
index 0000000..547b39d
--- /dev/null
@@ -0,0 +1,20 @@
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    <span>[% l('Serials Templates') %]</span>
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-md-3">
+    <div class="input-group">
+      <span class="input-group-addon">[% l('Owning Library') %]</span>
+      <eg-org-selector selected="context_ou"></eg-org-selector>
+    </div>
+  </div>
+</div>
+
+<div class="pad-vert"></div>
+
+<div>
+[% INCLUDE 'staff/admin/serials/t_template_list.tt2' %]
+</div>
index e989fcf..a8b8970 100644 (file)
             </a>
           </li>
           <li>
+            <a href="./admin/serials/index" target="_self">
+              <span class="glyphicon glyphicon-paperclip"></span>
+              [% l('Serials Administration') %]
+            </a>
+          </li>
+          <li>
             <a href="./admin/booking/index" target="_self">
               <span class="glyphicon glyphicon-calendar"></span>
               [% l('Booking Administration') %]
diff --git a/Open-ILS/web/js/ui/default/staff/admin/serials/app.js b/Open-ILS/web/js/ui/default/staff/admin/serials/app.js
new file mode 100644 (file)
index 0000000..a1f67ad
--- /dev/null
@@ -0,0 +1,576 @@
+angular.module('egSerialsAdmin',
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
+
+.config(['$routeProvider','$locationProvider','$compileProvider', 
+ function($routeProvider , $locationProvider , $compileProvider) {
+
+    $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); 
+    var resolver = {delay : function(egStartup) {return egStartup.go()}};
+
+    $routeProvider.when('/admin/serials/templates', {
+        templateUrl: './admin/serials/t_templates',
+        controller: 'TemplatesCtrl',
+        resolve : resolver
+    });
+
+    // default page 
+    $routeProvider.otherwise({
+        templateUrl : './admin/serials/t_splash',
+        resolve : resolver
+    });
+}])
+
+// cheating
+.factory("sharedScope",function(){
+    return {};
+})
+
+.factory('templateSvc', 
+       ['egCore','$q','$uibModal','ngToast',
+function(egCore , $q , $uibModal , ngToast ) {
+
+    var service = {
+    };
+
+    service.create_or_edit_template = function(id,cb) {
+        $uibModal.open({
+            template: '<eg-serials-template template_id="' + id + '"></eg-serials-template>',
+            controller:
+                   ['sharedScope','$uibModalInstance',
+            function(sharedScope , $uibModalInstance ) {
+                sharedScope.close_modal = function(count) { $uibModalInstance.close({}) }
+            }],
+            windowClass: 'app-modal-window',
+            backdrop: 'static',
+            keyboard: false
+        }).result.then(
+            function(args) {
+                if (cb) { cb(); }
+            }
+        );
+    }
+
+    service.delete_template = function(id,cb) {
+        return egCore.pcrud.search('act',
+            {id : id},
+            null, {atomic : true}
+        ).then(function(resp) {
+            var evt = egCore.evt.parse(resp);
+            if (evt) { console.log(evt); }
+            if (!evt && resp && resp.length > 0) {
+                return resp[0];
+            }
+        }).then(function(resp) {
+            resp.isdeleted(true); // needed?
+            return egCore.pcrud.remove(resp);
+        }).then(
+            function(resp) {
+                console.log(resp);
+                ngToast.success(egCore.strings.SERIALS_TEMPLATE_SUCCESS_DELETE);
+            },function(resp) {
+                console.log(resp);
+                ngToast.danger(egCore.strings.SERIALS_TEMPLATE_FAIL_DELETE);
+            }
+        ).finally(function() {
+            if (cb) { cb(); }
+        });
+    }
+
+    return service;
+}])
+
+.factory('itemSvc', 
+       ['egCore','$q',
+function(egCore , $q) {
+
+    var service = {
+    };
+
+    service.get_locations = function(orgs) {
+        return egCore.pcrud.search('acpl',
+            {owning_lib : orgs},
+            {order_by : { acpl : 'name' }}, {atomic : true}
+        );
+    };
+
+    service.get_statuses = function() {
+        if (egCore.env.ccs)
+            return $q.when(egCore.env.ccs.list);
+
+        return egCore.pcrud.retrieveAll('ccs', {order_by : { ccs : 'name' }}, {atomic : true}).then(
+            function(list) {
+                egCore.env.absorbList(list, 'ccs');
+                return list;
+            }
+        );
+
+    };
+
+    service.get_circ_mods = function() {
+        if (egCore.env.ccm)
+            return $q.when(egCore.env.ccm.list);
+
+        return egCore.pcrud.retrieveAll('ccm', {}, {atomic : true}).then(
+            function(list) {
+                egCore.env.absorbList(list, 'ccm');
+                return list;
+            }
+        );
+
+    };
+
+    service.get_circ_types = function() {
+        if (egCore.env.citm)
+            return $q.when(egCore.env.citm.list);
+
+        return egCore.pcrud.retrieveAll('citm', {}, {atomic : true}).then(
+            function(list) {
+                egCore.env.absorbList(list, 'citm');
+                return list;
+            }
+        );
+
+    };
+
+    service.get_age_protects = function() {
+        if (egCore.env.crahp)
+            return $q.when(egCore.env.crahp.list);
+
+        return egCore.pcrud.retrieveAll('crahp', {}, {atomic : true}).then(
+            function(list) {
+                egCore.env.absorbList(list, 'crahp');
+                return list;
+            }
+        );
+
+    };
+
+    service.get_floating_groups = function() {
+        if (egCore.env.cfg)
+            return $q.when(egCore.env.cfg.list);
+
+        return egCore.pcrud.retrieveAll('cfg', {}, {atomic : true}).then(
+            function(list) {
+                egCore.env.absorbList(list, 'cfg');
+                return list;
+            }
+        );
+
+    };
+
+    service.bmp_parts = {};
+    service.get_parts = function(rec) {
+        if (service.bmp_parts[rec])
+            return $q.when(service.bmp_parts[rec]);
+
+        return egCore.pcrud.search('bmp',
+            {record : rec, deleted : 'f'},
+            null, {atomic : true}
+        ).then(function(list) {
+            service.bmp_parts[rec] = list;
+            return list;
+        });
+
+    };
+
+    return service;
+}])
+
+.controller('TemplatesCtrl', 
+       ['$scope','$q','$window','$routeParams','$location','$timeout','egCore','egNet','itemSvc','templateSvc',
+        'egGridDataProvider',
+function($scope , $q , $window , $routeParams , $location , $timeout , egCore , egNet , itemSvc , templateSvc ,
+         egGridDataProvider ) {
+
+    function current_query() {
+        var filter = {
+            'owning_lib' : egCore.org.descendants($scope.context_ou.id(), true)
+        };
+        return filter;
+    }
+
+    function refresh_page() {
+        $scope.grid_controls.setQuery(current_query());
+    }
+
+    $scope.grid_actions = {
+        create_template : function() {
+            templateSvc.create_or_edit_template(null,refresh_page);
+        },
+        edit_template : function(items) {
+            templateSvc.create_or_edit_template(items[0].id,refresh_page);
+        },
+        delete_template : function(items) {
+            var promises = [];
+            angular.forEach(items,function(item) {
+                promises.push(templateSvc.delete_template(item.id));
+            });
+            $q.all(promises).then(function() {
+                refresh_page();
+            });
+        }
+    }
+    $scope.grid_controls = {
+        activateItem : function(item) {
+            templateSvc.create_or_edit_template(item.id,refresh_page);
+        },
+        setQuery : function(x) { return x || current_query(); },
+        setSort : function() { return ['name','id'] }
+    }
+
+    $scope.need_one_selected = function() {
+        var items = $scope.grid_controls.selectedItems();
+        if (items.length == 1) return false;
+        return true;
+    };
+
+    // called after any egGridActions action occurs
+    $scope.grid_actions.refresh = refresh_page;
+
+    // re-draw the grid when user changes the org selector
+    $scope.context_ou = egCore.org.get(egCore.auth.user().ws_ou());
+    $scope.$watch('context_ou', function(newVal, oldVal) {
+        if (newVal && newVal != oldVal) 
+            refresh_page();
+    });
+
+    refresh_page();
+
+}])
+
+.directive("egSerialsTemplate", function () {
+    return {
+        restrict: 'E',
+        replace: true,
+        template: '<div ng-include="'+"'/eg/staff/admin/serials/t_attr_edit'"+'"></div>',
+        scope: {
+            templateId: '=',
+        },
+        controller : ['$scope','$q','$window','itemSvc','egCore','ngToast','sharedScope',
+            function ( $scope , $q , $window , itemSvc , egCore , ngToast , sharedScope ) {
+
+                $scope.close_modal = function() {
+                    if ($scope.dirty && !window.confirm(egCore.strings.CONFIRM_DIRTY_EXIT)) {
+                        return;
+                    }
+                    //console.log('unsetting dirty for close_modal');
+                    $scope.dirty = false;
+                    sharedScope.close_modal();
+                };
+
+                $scope.defaults = { // If defaults are not set at all, allow everything
+                    attributes : {
+                        status : true,
+                        loan_duration : true,
+                        fine_level : true,
+                        alerts : true,
+                        deposit : true,
+                        deposit_amount : true,
+                        opac_visible : true,
+                        price : true,
+                        circulate : true,
+                        mint_condition : true,
+                        circ_lib : true,
+                        ref : true,
+                        circ_modifier : true,
+                        circ_as_type : true,
+                        location : true,
+                        holdable : true,
+                        age_protect : true,
+                        floating : true
+                    }
+                };
+
+                $scope.fetchDefaults = function () {
+                    egCore.hatch.getItem('serials.copy.defaults').then(function(t) {
+                        if (t) {
+                            $scope.defaults = t;
+                        }
+                    });
+                }
+                $scope.fetchDefaults();
+
+                //console.log('unsetting dirty by default');
+                $scope.dirty = false;
+                $scope.$watch('dirty',
+                    function(newVal, oldVal) {
+                        //console.log('watching dirty');
+                        //console.log('...oldVal',oldVal);
+                        //console.log('...newVal',newVal);
+                        //console.log('...fetching',$scope.fetching);
+                        if (newVal && $scope.fetching) {
+                            // KLUDGY
+                            // so after fetchTemplate -> applyTemplate
+                            // the working watches will fire and set
+                            // dirty to true.  We'll undo that at this
+                            // point.
+                            //console.log('unsetting dirty via kludge');
+                            $scope.fetching = false;
+                            $scope.dirty = false;
+                            newVal = false;
+                        }
+                        if (newVal && newVal != oldVal) {
+                            $($window).on('beforeunload.template', function(){
+                                return 'There is unsaved template data!'
+                            });
+                        } else {
+                            $($window).off('beforeunload.template');
+                        }
+                    }
+                );
+
+                $scope.applyTemplate = function() {
+                    //console.log('applying...');
+                    angular.forEach($scope.hashed_template, function (v,k) {
+                        //console.log(k,v);
+                        if (k == 'circ_lib') {
+                            $scope.working[k] = egCore.org.get(v);
+                        } else if (!angular.isObject(v)) {
+                            $scope.working[k] = angular.copy(v);
+                        } else {
+                            angular.forEach(v, function (sv,sk) {
+                                if (!(k in $scope.working))
+                                    $scope.working[k] = {};
+                                $scope.working[k][sk] = angular.copy(sv);
+                            });
+                        }
+                    });
+                    //console.log('unsetting dirty via applyTemplate');
+                    $scope.dirty = false;
+                }
+
+                $scope.fetching = false;
+                $scope.fetchTemplate = function () {
+                    $scope.fetching = true;
+                    return egCore.pcrud.search('act',
+                        {id : $scope.templateId},
+                        null, {atomic : true}
+                    ).then(function(resp) {
+                        var evt = egCore.evt.parse(resp);
+                        if (evt) { console.log(evt); }
+                        if (!evt && resp && resp.length > 0) {
+                            $scope.fm_template =  resp[0];
+                            $scope.hashed_template = egCore.idl.toHash(resp[0]); 
+                            $scope.applyTemplate();
+                        } else {
+                            console.log('new template');
+                        }
+                    });
+                }
+                $scope.saveTemplate = function() {
+                    var tmpl = {};
+        
+                    angular.forEach($scope.working, function (v,k) {
+                        if (angular.isObject(v)) { // we'll use the pkey
+                            if (v.id) v = v.id();
+                            else if (v.code) v = v.code();
+                        }
+        
+                        tmpl[k] = v;
+                    });
+        
+                    $scope.hashed_template = tmpl;
+
+                    var act_obj = $scope.fm_template || new egCore.idl.act() ;
+                    //console.log('consuming...');
+                    angular.forEach($scope.hashed_template, function (v,k) {
+                        //console.log(k,v);
+                        if (typeof act_obj[k] == 'function') {
+                            act_obj[k](v);
+                        } else {
+                            console.log('something wrong here',k,act_obj[k]);
+                        }
+                    });
+                    if ($scope.fm_template) {
+                        console.log('edit');
+                        act_obj.ischanged('t');
+                        act_obj.editor( egCore.auth.user().id() );
+                        act_obj.edit_date( new Date() );
+                    } else {
+                        console.log('create');
+                        act_obj.isnew('t');
+                        act_obj.creator( egCore.auth.user().id() );
+                        act_obj.owning_lib( egCore.auth.user().ws_ou() );
+                        act_obj.create_date( new Date() );
+                    }
+                    var some_failure = false;
+                    var some_success = false;
+                    egCore.net.request(
+                        'open-ils.cat', // worth replacing with pcrud?
+                        'open-ils.cat.asset.copy_template.create_or_update',
+                        egCore.auth.token(),
+                        act_obj
+                    ).then(
+                        function(resp) {
+                            var evt = egCore.evt.parse(resp);
+                            if (evt) { // any way to just throw or return this to the error handler?
+                                console.log('failure',resp);
+                                some_failure = true;
+                                ngToast.danger(egCore.strings.SERIALS_TEMPLATE_FAIL_SAVE);
+                            } else {
+                                console.log('success',resp);
+                                some_success = true;
+                                ngToast.success(egCore.strings.SERIALS_TEMPLATE_SUCCESS_SAVE);
+                            }
+                        },
+                        function(resp) {
+                            console.log('failure',resp);
+                            some_failure = true;
+                            ngToast.danger(egCore.strings.SERIALS_TEMPLATE_FAIL_SAVE);
+                        }
+                    ).then(function(){
+                        if (some_success && !some_failure) {
+                            //console.log('unsetting dirty for save');
+                            $scope.dirty = false;
+                            $scope.close_modal();
+                        }
+                    });
+                }
+            
+                $scope.hashed_template = {};
+                $scope.imported_template = { data : '' };
+                $scope.fetchTemplate();
+
+                // FIXME - leaving this for now
+                $scope.$watch('imported_template.data', function(newVal, oldVal) {
+                    if (newVal && newVal != oldVal) {
+                        try {
+                            var newTemplate = JSON.parse(newVal);
+                            if (!Object.keys(newTemplate).length) return;
+                            $scope.hashed_template = newTemplate;
+                        } catch (E) {
+                            console.log('tried to import an invalid serials template file');
+                        }
+                    }
+                });
+
+                $scope.orgById = function (id) { return egCore.org.get(id) }
+                $scope.statusById = function (id) {
+                    return $scope.status_list.filter( function (s) { return s.id() == id } )[0];
+                }
+                $scope.locationById = function (id) {
+                    return $scope.location_cache[''+id];
+                }
+            
+                createSimpleUpdateWatcher = function (field) {
+                    $scope.$watch('working.' + field, function () {
+                        var newval = $scope.working[field];
+            
+                        if (typeof newval != 'undefined') {
+                            //console.log('setting dirty for field',field);
+                            $scope.dirty = true;
+                            if (angular.isObject(newval)) { // we'll use the pkey
+                                if (newval.id) $scope.working[field] = newval.id();
+                                else if (newval.code) $scope.working[field] = newval.code();
+                            }
+            
+                            if (""+newval == "" || newval == null) {
+                                $scope.working[field] = undefined;
+                            }
+            
+                        }
+                    });
+                }
+
+                $scope.clearWorking = function () {
+                    angular.forEach($scope.working, function (v,k,o) {
+                        if (!angular.isObject(v)) {
+                            if (typeof v != 'undefined')
+                                $scope.working[k] = undefined;
+                        } else if (k != 'circ_lib') {
+                            angular.forEach(v, function (sv,sk) {
+                                $scope.working[k][sk] = undefined;
+                            });
+                        }
+                    });
+                    $scope.working.circ_lib = undefined; // special
+                    //console.log('unsetting dirty for clearWorking');
+                    $scope.dirty = false;
+                }
+
+                $scope.working = {};
+                $scope.location_orgs = [];
+                $scope.location_cache = {};
+            
+                $scope.location_list = [];
+                itemSvc.get_locations(
+                    egCore.org.fullPath( egCore.auth.user().ws_ou(), true )
+                ).then(function(list){
+                    $scope.location_list = list;
+                });
+                createSimpleUpdateWatcher('location');
+
+                $scope.status_list = [];
+                itemSvc.get_statuses().then(function(list){
+                    $scope.status_list = list;
+                });
+                createSimpleUpdateWatcher('status');
+            
+                $scope.circ_modifier_list = [];
+                itemSvc.get_circ_mods().then(function(list){
+                    $scope.circ_modifier_list = list;
+                });
+                createSimpleUpdateWatcher('circ_modifier');
+            
+                $scope.circ_type_list = [];
+                itemSvc.get_circ_types().then(function(list){
+                    $scope.circ_type_list = list;
+                });
+                createSimpleUpdateWatcher('circ_as_type');
+            
+                $scope.age_protect_list = [];
+                itemSvc.get_age_protects().then(function(list){
+                    $scope.age_protect_list = list;
+                });
+                createSimpleUpdateWatcher('age_protect');
+            
+                createSimpleUpdateWatcher('circulate');
+                createSimpleUpdateWatcher('holdable');
+
+                $scope.loan_duration_options = [
+                    {
+                        v: function(){return 1;},
+                        l: function(){return egCore.strings.LOAN_DURATION_SHORT;}
+                    },
+                    {
+                        v: function(){return 2;},
+                        l: function(){return egCore.strings.LOAN_DURATION_NORMAL;}
+                    },
+                    {
+                        v: function(){return 3;},
+                        l: function(){return egCore.strings.LOAN_DURATION_EXTENDED;}
+                    }
+                ];
+                createSimpleUpdateWatcher('loan_duration');
+
+                $scope.fine_level_options = [
+                    {
+                        v: function(){return 1;},
+                        l: function(){return egCore.strings.FINE_LEVEL_LOW;}
+                    },
+                    {
+                        v: function(){return 2;},
+                        l: function(){return egCore.strings.FINE_LEVEL_NORMAL;}
+                    },
+                    {
+                        v: function(){return 3;},
+                        l: function(){return egCore.strings.FINE_LEVEL_HIGH;}
+                    }
+                ];
+                createSimpleUpdateWatcher('fine_level');
+
+                createSimpleUpdateWatcher('name');
+                createSimpleUpdateWatcher('price');
+                createSimpleUpdateWatcher('deposit');
+                createSimpleUpdateWatcher('deposit_amount');
+                createSimpleUpdateWatcher('mint_condition');
+                createSimpleUpdateWatcher('opac_visible');
+                createSimpleUpdateWatcher('ref');
+            }
+        ]
+    }
+})
+
+