webstaff: reorganize serials app layout
authorGalen Charlton <gmc@equinoxinitiative.org>
Mon, 24 Apr 2017 21:05:19 +0000 (17:05 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Mon, 24 Apr 2017 21:05:19 +0000 (17:05 -0400)
This patch lays the groundwork for breaking out each tab
in the main page into a separate directive, and establishes
the directive for the subscription manager. Note that at present
stuff under serials/directives is expected to be firmly bound
with other aspects of the over serials app, including its
services.

Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Open-ILS/src/templates/staff/serials/index.tt2
Open-ILS/src/templates/staff/serials/t_manage.tt2
Open-ILS/src/templates/staff/serials/t_subscription_manager.tt2 [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/serials/app.js
Open-ILS/web/js/ui/default/staff/serials/directives/subscription_manager.js [new file with mode: 0644]

index bbb73ac..3ae1ea8 100644 (file)
@@ -8,6 +8,7 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/serials/services/core.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/serials/app.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/serials/directives/subscription_manager.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/record.js"></script>
 
 [% END %]
index f4e4a38..58bfc20 100644 (file)
@@ -8,112 +8,7 @@
       <!-- note that non-numeric index values must be enclosed in single-quotes,
            otherwise selecting the active table won't work cleanly -->
       <uib-tab index="'create-subscription'" heading="[% l('Create Subscription') %]">
-<!-- TODO: move the subscription / distribution manager to separate template -->
-<form name="ssubform">
-  <div ng-repeat="ssub in subscriptions">
-    <div class="row form-inline">
-      <div class="form-group">
-        <label>[% l('Owning Library') %]</label>
-        <eg-org-selector selected="ssub.owning_lib"></eg-org-selector>
-      </div>
-      <div class="form-group">
-        <label>[% l('Start Date') %]</label>
-      </div>
-      <div class="form-group">
-        <eg-date-input ng-model="ssub.start_date"></eg-date-input>
-      </div>
-      <div class="form-group">
-        <label>[% l('End Date') %]</label>
-      </div>
-      <div class="form-group">
-        <eg-date-input ng-model="ssub.end_date"></eg-date-input>
-      </div>
-      <div class="form-group">
-        <label>[% l('Expected Offset') %]</label>
-        <input type="text" ng-model="ssub.expected_offset"></input>
-      </div>
-      <button class="btn btn-sm btn-warning" ng-click="add_distribution(ssub)">[% l('Add distribution') %]</button>
-    </div>
-    <div class="row form-inline" ng-repeat="sdist in ssub.distributions">
-      <div class="col-md-1"></div>
-      <div class="col-md-5 form-group">
-        <label>[% l('Library') %]</label>
-        <eg-org-selector selected="sdist.holding_lib"></eg-org-selector>
-        <label>[% l('Label') %]</label>
-        <input type="text" required ng-model="sdist.label"></input>
-        <label>[% l('OPAC Display') %]</label>
-        <select required ng-model="sdist.display_grouping">
-          <option value="chron">[% l('Chronological') %]</option>
-          <option value="enum" >[% l('Enumeration') %]</option>
-        </select>
-      </div>
-      <div class="col-md-2">
-        <button class="btn btn-sm btn-info" ng-click="add_stream(sdist)">[% l('Add copy stream') %]</button>
-      </div>
-      <div class="col-md-3">
-        <div class="row" ng-repeat="sstr in sdist.streams">
-            <div class="form-inline">
-              <label>[% l('Send to') %]</label>
-              <input type="text" ng-model="sstr.routing_label"></input>
-            </div>
-        </div>
-      </div>
-    </div>
-    <div class="row pad-vert"></div>
-  </div>
-  <div class="row">
-    <button class="btn btn-warning pull-left" ng-click="add_subscription()">[% l('New Subscription') %]</button>
-    <div class="btn-group pull-right">
-      <button class="btn btn-default" ng-disabled="!ssubform.$dirty" ng-click="abort_changes(ssubform)">[% l('Cancel') %]</button>
-      <button class="btn btn-primary" ng-disabled="!ssubform.$dirty" ng-click="save_subscriptions(ssubform)">[% l('Save') %]</button>
-    </div>
-  </div>
-  <div class="row pad-vert"></div>
-</form>
-<!-- TODO: move the grid to a separate template file -->
-<div>
-  <eg-grid
-    id-field="index"
-    features="-display,-sort,-multisort"
-    items-provider="distStreamGridDataProvider"
-    grid-controls="distStreamGridControls"
-    persist-key="serials.dist_stream_grid">
-
-    <eg-grid-action handler="apply_receiving_template"
-      label="[% l('Apply Receiving Template') %]"></eg-grid-action>
-    <eg-grid-action handler="apply_binding_template"
-      label="[% l('Apply Binding Template') %]"></eg-grid-action>
-    <eg-grid-action handler="additional_routing" disabled="need_one_selected"
-      label="[% l('Additional Routing') %]"></eg-grid-action>
-    <eg-grid-action handler="link_mfhd"
-      label="[% l('Link MFHD') %]"></eg-grid-action>
-    <eg-grid-action handler="edit_offsets"
-      label="[% l('Edit Offsets') %]"></eg-grid-action>
-    <eg-grid-action handler="clone_subscription"
-      label="[% l('Clone Subscription') %]"></eg-grid-action>
-
-    <eg-grid-field label="[% l('Owning Library') %]" path="owning_lib.name" visible></eg-grid-field>
-    <eg-grid-field label="[% l('Distribution Library') %]" path="sdist.holding_lib.name" visible></eg-grid-field>
-    <eg-grid-field label="[% l('Distribution Label') %]" path="sdist.label" visible></eg-grid-field>
-    <eg-grid-field label="[% l('Copy Stream') %]" path="sstr.id" visible></eg-grid-field>
-    <eg-grid-field label="[% l('Offset') %]" path="expected_date_offset" visible></eg-grid-field>
-    <eg-grid-field label="[% l('Start Date') %]" path="start_date" datatype="timestamp" visible></eg-grid-field>
-    <eg-grid-field label="[% l('End Date') %]" path="end_date" datatype="timestamp" visible></eg-grid-field>
-    <eg-grid-field label="[% l('Route To') %]" path="sstr.routing_label" visible></eg-grid-field>
-    <eg-grid-field label="[% l('Additional Routing') %]" path="sstr.additional_routing" visible></eg-grid-field>
-    <eg-grid-field label="[% l('Receiving Template') %]" path="sdist.receive_unit_template.name" visible></eg-grid-field>
-    <eg-grid-field label="[% l('MFHD ID') %]" path="sdist.record_entry" visible></eg-grid-field>
-    <eg-grid-field label="[% l('Summary Display') %]" path="sdist.summary_method" visible></eg-grid-field>
-    <eg-grid-field label="[% l('Receiving Call Number') %]" path="sdist.receive_call_number.label"></eg-grid-field>
-    <eg-grid-field label="[% l('Binding Call Number') %]" path="sdist.bind_call_number.label"></eg-grid-field>
-    <eg-grid-field label="[% l('Binding Template') %]" path="sdist.bind_unit_template.name"></eg-grid-field>
-    <eg-grid-field label="[% l('Unit Label Prefix') %]" path="sdist.unit_label_prefix"></eg-grid-field>
-    <eg-grid-field label="[% l('Unit Label Suffix') %]" path="sdist.unit_label_suffix"></eg-grid-field>
-    <eg-grid-field label="[% l('Display Grouping') %]" path="sdist.display_grouping"></eg-grid-field>
-    <eg-grid-field label="[% l('Subscription ID') %]" path="id"></eg-grid-field>
-    <eg-grid-field label="[% l('Distribution ID') %]" path="sdist.id"></eg-grid-field>
-  </eg-grid>
-</div>
+        <eg-subscription-manager bib-id="bib_id"></eg-subscription-manager>
       </uib-tab>
       <uib-tab index="'prediction'" heading="[% l('Manage Predictions') %]">
         <p>Frequency TODO</p>
diff --git a/Open-ILS/src/templates/staff/serials/t_subscription_manager.tt2 b/Open-ILS/src/templates/staff/serials/t_subscription_manager.tt2
new file mode 100644 (file)
index 0000000..1c15c0a
--- /dev/null
@@ -0,0 +1,104 @@
+<form name="ssubform">
+  <div ng-repeat="ssub in subscriptions">
+    <div class="row form-inline">
+      <div class="form-group">
+        <label>[% l('Owning Library') %]</label>
+        <eg-org-selector selected="ssub.owning_lib"></eg-org-selector>
+      </div>
+      <div class="form-group">
+        <label>[% l('Start Date') %]</label>
+      </div>
+      <div class="form-group">
+        <eg-date-input ng-model="ssub.start_date"></eg-date-input>
+      </div>
+      <div class="form-group">
+        <label>[% l('End Date') %]</label>
+      </div>
+      <div class="form-group">
+        <eg-date-input ng-model="ssub.end_date"></eg-date-input>
+      </div>
+      <div class="form-group">
+        <label>[% l('Expected Offset') %]</label>
+        <input type="text" ng-model="ssub.expected_offset"></input>
+      </div>
+      <button class="btn btn-sm btn-warning" ng-click="add_distribution(ssub)">[% l('Add distribution') %]</button>
+    </div>
+    <div class="row form-inline" ng-repeat="sdist in ssub.distributions">
+      <div class="col-md-1"></div>
+      <div class="col-md-5 form-group">
+        <label>[% l('Library') %]</label>
+        <eg-org-selector selected="sdist.holding_lib"></eg-org-selector>
+        <label>[% l('Label') %]</label>
+        <input type="text" required ng-model="sdist.label"></input>
+        <label>[% l('OPAC Display') %]</label>
+        <select required ng-model="sdist.display_grouping">
+          <option value="chron">[% l('Chronological') %]</option>
+          <option value="enum" >[% l('Enumeration') %]</option>
+        </select>
+      </div>
+      <div class="col-md-2">
+        <button class="btn btn-sm btn-info" ng-click="add_stream(sdist)">[% l('Add copy stream') %]</button>
+      </div>
+      <div class="col-md-3">
+        <div class="row" ng-repeat="sstr in sdist.streams">
+            <div class="form-inline">
+              <label>[% l('Send to') %]</label>
+              <input type="text" ng-model="sstr.routing_label"></input>
+            </div>
+        </div>
+      </div>
+    </div>
+    <div class="row pad-vert"></div>
+  </div>
+  <div class="row">
+    <button class="btn btn-warning pull-left" ng-click="add_subscription()">[% l('New Subscription') %]</button>
+    <div class="btn-group pull-right">
+      <button class="btn btn-default" ng-disabled="!ssubform.$dirty" ng-click="abort_changes(ssubform)">[% l('Cancel') %]</button>
+      <button class="btn btn-primary" ng-disabled="!ssubform.$dirty" ng-click="save_subscriptions(ssubform)">[% l('Save') %]</button>
+    </div>
+  </div>
+  <div class="row pad-vert"></div>
+</form>
+<div>
+  <eg-grid
+    id-field="index"
+    features="-display,-sort,-multisort"
+    items-provider="distStreamGridDataProvider"
+    grid-controls="distStreamGridControls"
+    persist-key="serials.dist_stream_grid">
+
+    <eg-grid-action handler="apply_receiving_template"
+      label="[% l('Apply Receiving Template') %]"></eg-grid-action>
+    <eg-grid-action handler="apply_binding_template"
+      label="[% l('Apply Binding Template') %]"></eg-grid-action>
+    <eg-grid-action handler="additional_routing" disabled="need_one_selected"
+      label="[% l('Additional Routing') %]"></eg-grid-action>
+    <eg-grid-action handler="link_mfhd"
+      label="[% l('Link MFHD') %]"></eg-grid-action>
+    <eg-grid-action handler="edit_offsets"
+      label="[% l('Edit Offsets') %]"></eg-grid-action>
+    <eg-grid-action handler="clone_subscription"
+      label="[% l('Clone Subscription') %]"></eg-grid-action>
+
+    <eg-grid-field label="[% l('Owning Library') %]" path="owning_lib.name" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Distribution Library') %]" path="sdist.holding_lib.name" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Distribution Label') %]" path="sdist.label" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Copy Stream') %]" path="sstr.id" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Offset') %]" path="expected_date_offset" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Start Date') %]" path="start_date" datatype="timestamp" visible></eg-grid-field>
+    <eg-grid-field label="[% l('End Date') %]" path="end_date" datatype="timestamp" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Route To') %]" path="sstr.routing_label" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Additional Routing') %]" path="sstr.additional_routing" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Receiving Template') %]" path="sdist.receive_unit_template.name" visible></eg-grid-field>
+    <eg-grid-field label="[% l('MFHD ID') %]" path="sdist.record_entry" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Summary Display') %]" path="sdist.summary_method" visible></eg-grid-field>
+    <eg-grid-field label="[% l('Receiving Call Number') %]" path="sdist.receive_call_number.label"></eg-grid-field>
+    <eg-grid-field label="[% l('Binding Call Number') %]" path="sdist.bind_call_number.label"></eg-grid-field>
+    <eg-grid-field label="[% l('Binding Template') %]" path="sdist.bind_unit_template.name"></eg-grid-field>
+    <eg-grid-field label="[% l('Unit Label Prefix') %]" path="sdist.unit_label_prefix"></eg-grid-field>
+    <eg-grid-field label="[% l('Unit Label Suffix') %]" path="sdist.unit_label_suffix"></eg-grid-field>
+    <eg-grid-field label="[% l('Display Grouping') %]" path="sdist.display_grouping"></eg-grid-field>
+    <eg-grid-field label="[% l('Subscription ID') %]" path="id"></eg-grid-field>
+    <eg-grid-field label="[% l('Distribution ID') %]" path="sdist.id"></eg-grid-field>
+  </eg-grid>
+</div>
index c494e56..50493a1 100644 (file)
@@ -1,5 +1,7 @@
-angular.module('egSerialsApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod','ngToast','egSerialsMod'])
+angular.module('egSerialsApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod','ngToast','egSerialsMod','egSerialsAppDep']);
+angular.module('egSerialsAppDep', []);
 
+angular.module('egSerialsApp')
 .config(['ngToastProvider', function(ngToastProvider) {
   ngToastProvider.configure({
     verticalPosition: 'bottom',
@@ -27,289 +29,8 @@ angular.module('egSerialsApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod'
 })
 
 .controller('ManageCtrl',
-       ['$scope','$routeParams','$location','$window','$q','egSerialsCoreSvc','egCore',
-        'egGridDataProvider','$uibModal',
-function($scope , $routeParams , $location , $window , $q , egSerialsCoreSvc , egCore ,
-         egGridDataProvider , $uibModal
-) {
+       ['$scope','$routeParams','$location',
+function($scope , $routeParams , $location) {
     $scope.bib_id = $routeParams.bib_id;
     $scope.active_tab = $routeParams.active_tab ?  $routeParams.active_tab : 'create-subscription';
-    egSerialsCoreSvc.fetch($scope.bib_id).then(function() {
-        $scope.subscriptions = egCore.idl.toTypedHash(egSerialsCoreSvc.subTree);
-    });
-    $scope.distStreamGridControls = {
-        activateItem : function (item) { } // TODO
-    };
-    $scope.distStreamGridDataProvider = egGridDataProvider.instance({
-        get : function(offset, count) {
-            return this.arrayNotifier(egSerialsCoreSvc.subList, offset, count);
-        }
-    });
-
-    function reload() {
-        egSerialsCoreSvc.fetch($scope.bib_id).then(function() {
-            $scope.subscriptions = egCore.idl.toTypedHash(egSerialsCoreSvc.subTree);
-            $scope.distStreamGridDataProvider.refresh();
-        });
-    }
-
-    $scope.need_one_selected = function() {
-        var items = $scope.distStreamGridControls.selectedItems();
-        if (items.length == 1) return false;
-        return true;
-    };
-
-    $scope.add_subscription = function() {
-        var new_ssub = egCore.idl.toTypedHash(new egCore.idl.ssub());
-        new_ssub._isnew = true;
-        new_ssub.record_entry = $scope.bib_id;
-        $scope.subscriptions.push(new_ssub);
-    }
-    $scope.add_distribution = function(ssub) {
-        var new_sdist = egCore.idl.toTypedHash(new egCore.idl.sdist());
-        new_sdist._isnew = true;
-        new_sdist.subscription = ssub.id;
-        if (!angular.isArray(ssub.distributions)){
-            ssub.distributions = [];
-        }
-        ssub.distributions.push(new_sdist);
-    }
-    $scope.add_stream = function(sdist) {
-        var new_sstr = egCore.idl.toTypedHash(new egCore.idl.sstr());
-        new_sstr.distribution = sdist.id;
-        new_sstr._isnew = true;
-        if (!angular.isArray(sdist.streams)){
-            sdist.streams = [];
-        }
-        sdist.streams.push(new_sstr);
-    }
-
-    $scope.abort_changes = function(form) {
-        reload();
-        form.$setPristine();
-    }
-    $scope.save_subscriptions = function(form) {
-        // traverse through structure and set _ischanged
-        // TODO add more granular dirty input detection
-        angular.forEach($scope.subscriptions, function(ssub) {
-            if (!ssub._isnew) ssub._ischanged = true;
-            angular.forEach(ssub.distributions, function(sdist) {
-                if (!sdist._isnew) sdist._ischanged = true;
-                angular.forEach(sdist.streams, function(sstr) {
-                    if (!sstr._isnew) sstr._ischanged = true;
-                });
-            });
-        });
-
-        var obj = egCore.idl.fromTypedHash($scope.subscriptions);
-
-        // create a bunch of promises that each get resolved upon each
-        // CUD update; that way, we can know when the entire save
-        // operation is completed
-        var promises = [];
-        angular.forEach(obj, function(ssub) {
-            ssub._cud_done = $q.defer();
-            promises.push(ssub._cud_done.promise);
-            angular.forEach(ssub.distributions(), function(sdist) {
-                sdist._cud_done = $q.defer();
-                promises.push(sdist._cud_done.promise);
-                angular.forEach(sdist.streams(), function(sstr) {
-                    sstr._cud_done = $q.defer();
-                    promises.push(sstr._cud_done.promise);
-                });
-            });
-        });
-
-        angular.forEach(obj, function(ssub) {
-            ssub.owning_lib(ssub.owning_lib().id()); // deflesh
-            egCore.pcrud.apply(ssub).then(function(res) {
-                var ssub_id = (ssub.isnew() && angular.isObject(res)) ? res.id() : ssub.id();
-                angular.forEach(ssub.distributions(), function(sdist) {
-                    // set subscription ID just in case it's new
-                    sdist.holding_lib(sdist.holding_lib().id()); // deflesh
-                    sdist.subscription(ssub_id);
-                    egCore.pcrud.apply(sdist).then(function(res) {
-                        var sdist_id = (sdist.isnew() && angular.isObject(res)) ? res.id() : sdist.id();
-                        angular.forEach(sdist.streams(), function(sstr) {
-                            // set distribution ID just in case it's new
-                            sstr.distribution(sdist_id);
-                            egCore.pcrud.apply(sstr).then(function(res) {
-                                sstr._cud_done.resolve();
-                            });
-                        });
-                    });
-                    sdist._cud_done.resolve();
-                });
-                ssub._cud_done.resolve();
-            });
-        });
-        $q.all(promises).then(function(resolutions) {
-            reload();
-            form.$setPristine();
-        });
-    }
-    $scope.additional_routing = function(rows) {
-        if (!rows) { return; }
-        var row = rows[0];
-        if (!row) { row = $scope.distStreamGridControls.selectedItems()[0]; }
-        if (row && row['sstr.id']) {
-            egCore.pcrud.search('srlu', {
-                    stream : row['sstr.id']
-                }, {
-                    flesh : 2,
-                    flesh_fields : {
-                        'srlu' : ['reader'],
-                        'au'  : ['mailing_address','billing_address','home_ou']
-                    },
-                    order_by : { srlu : 'pos' }
-                },
-                { atomic : true }
-            ).then(function(list) {
-                $uibModal.open({
-                    templateUrl: './serials/t_routing_list',
-                    controller: 'RoutingCtrl',
-                    resolve : {
-                        rowInfo : function() {
-                            return row;
-                        },
-                        routes : function() {
-                            return egCore.idl.toHash(list);
-                        }
-                    }
-                }).result.then(function(routes) {
-                    // delete all of the routes first;
-                    // it's easiest given the constraints
-                    var deletions = [];
-                    var creations = [];
-                    angular.forEach(routes, function(r) {
-                        var srlu = new egCore.idl.srlu();
-                        srlu.stream(r.stream);
-                        srlu.pos(r.pos);
-                        if (r.reader) {
-                            srlu.reader(r.reader.id);
-                        }
-                        srlu.department(r.department);
-                        srlu.note(r.note);
-                        if (r.id) {
-                            srlu.id(r.id);
-                            var srlu_copy = angular.copy(srlu);
-                            srlu_copy.isdeleted(true);
-                            deletions.push(srlu_copy);
-                        }
-                        if (!r.delete_me) {
-                            srlu.isnew(true);
-                            creations.push(srlu);
-                        }
-                    });
-                    egCore.pcrud.apply(deletions.concat(creations)).then(function(){
-                        reload();
-                    });
-                });
-            });
-        }
-    }
-}])
-
-.controller('RoutingCtrl',
-       ['$scope','$uibModalInstance','egCore','rowInfo','routes',
-function($scope , $uibModalInstance , egCore , rowInfo , routes ) {
-    $scope.args = {
-         which_radio_button: 'reader'
-        ,reader: ''
-        ,department: ''
-        ,delete_me: false
-    };
-    $scope.stream_id = rowInfo['sstr.id'];
-    $scope.stream_label = rowInfo['sstr.routing_label'];
-    $scope.routes = routes;
-    $scope.readerInFocus = true;
-    $scope.ok = function(count) { $uibModalInstance.close($scope.routes) }
-    $scope.cancel = function () { $uibModalInstance.dismiss() }
-    $scope.model_has_changed = false;
-    $scope.find_user = function () {
-
-        $scope.readerNotFound = null;
-        $scope.reader_obj = null;
-        if (!$scope.args.reader) return;
-
-        egCore.net.request(
-            'open-ils.actor',
-            'open-ils.actor.get_barcodes',
-            egCore.auth.token(), egCore.auth.user().ws_ou(),
-            'actor', $scope.args.reader)
-
-        .then(function(resp) { // get_barcodes
-
-            if (evt = egCore.evt.parse(resp)) {
-                console.error(evt.toString());
-                return;
-            }
-
-            if (!resp || !resp[0]) {
-                $scope.readerNotFound = $scope.args.reader;
-                return;
-            }
-
-            egCore.pcrud.search('au', {
-                    id : resp[0].id
-                }, {
-                    flesh : 1,
-                    flesh_fields : {
-                        'au'  : ['mailing_address','billing_address','home_ou']
-                    }
-                },
-                { atomic : true }
-            ).then(function(usr) {
-                $scope.reader_obj = egCore.idl.toHash(usr[0]);
-            });
-        });
-    }
-    $scope.add_route = function () {
-        var new_route = {
-             stream: $scope.stream_id
-            ,pos: $scope.routes.length
-            ,note: $scope.args.note
-        }
-        if ($scope.args.which_radio_button == 'reader') {
-            new_route.reader = $scope.reader_obj;
-        } else {
-            new_route.department = $scope.args.department;
-        }
-        $scope.routes.push(new_route);
-        $scope.model_has_changed = true;
-    }
-    function adjust_pos_field() {
-        var idx = 0;
-        for (var i = 0; i < $scope.routes.length; i++) {
-            $scope.routes[i].pos = $scope.routes[i].delete_me ? idx : idx++;
-        }
-        $scope.model_has_changed = true;
-    }
-    $scope.move_route_up = function(r) {
-        var pos = r.pos;
-        if (pos > 0) {
-            var temp = $scope.routes[ pos - 1 ];
-            $scope.routes[ pos - 1 ] = $scope.routes[ pos ];
-            $scope.routes[ pos ] = temp;
-            adjust_pos_field();
-        }
-    }
-    $scope.move_route_down = function(r) {
-        var pos = r.pos;
-        if (pos < $scope.routes.length - 1) {
-            var temp = $scope.routes[ pos + 1 ];
-            $scope.routes[ pos + 1 ] = $scope.routes[ pos ];
-            $scope.routes[ pos ] = temp;
-            adjust_pos_field();
-        }
-    }
-    $scope.toggle_delete = function(r) {
-        r.delete_me = ! r.delete_me;
-        adjust_pos_field();
-    }
-    $scope.$watch("args.reader", function(newVal, oldVal) {
-        if (newVal && newVal != oldVal) {
-            $scope.find_user();
-        }
-    });
 }])
diff --git a/Open-ILS/web/js/ui/default/staff/serials/directives/subscription_manager.js b/Open-ILS/web/js/ui/default/staff/serials/directives/subscription_manager.js
new file mode 100644 (file)
index 0000000..fb983ec
--- /dev/null
@@ -0,0 +1,296 @@
+angular.module('egSerialsAppDep')
+
+.directive('egSubscriptionManager', function() {
+    return {
+        transclude: true,
+        restrict:   'E',
+        scope: {
+            bibId : '='
+        },
+        templateUrl: './serials/t_subscription_manager',
+        controller:
+                   ['$scope','$q','egSerialsCoreSvc','egCore','egGridDataProvider',
+                    '$uibModal',
+            function($scope , $q , egSerialsCoreSvc , egCore , egGridDataProvider ,
+                     $uibModal) {
+    egSerialsCoreSvc.fetch($scope.bibId).then(function() {
+        $scope.subscriptions = egCore.idl.toTypedHash(egSerialsCoreSvc.subTree);
+    });
+    $scope.distStreamGridControls = {
+        activateItem : function (item) { } // TODO
+    };
+    $scope.distStreamGridDataProvider = egGridDataProvider.instance({
+        get : function(offset, count) {
+            return this.arrayNotifier(egSerialsCoreSvc.subList, offset, count);
+        }
+    });
+
+    function reload() {
+        egSerialsCoreSvc.fetch($scope.bibId).then(function() {
+            $scope.subscriptions = egCore.idl.toTypedHash(egSerialsCoreSvc.subTree);
+            $scope.distStreamGridDataProvider.refresh();
+        });
+    }
+
+    $scope.need_one_selected = function() {
+        var items = $scope.distStreamGridControls.selectedItems();
+        if (items.length == 1) return false;
+        return true;
+    };
+
+    $scope.add_subscription = function() {
+        var new_ssub = egCore.idl.toTypedHash(new egCore.idl.ssub());
+        new_ssub._isnew = true;
+        new_ssub.record_entry = $scope.bibId;
+        $scope.subscriptions.push(new_ssub);
+    }
+    $scope.add_distribution = function(ssub) {
+        var new_sdist = egCore.idl.toTypedHash(new egCore.idl.sdist());
+        new_sdist._isnew = true;
+        new_sdist.subscription = ssub.id;
+        if (!angular.isArray(ssub.distributions)){
+            ssub.distributions = [];
+        }
+        ssub.distributions.push(new_sdist);
+    }
+    $scope.add_stream = function(sdist) {
+        var new_sstr = egCore.idl.toTypedHash(new egCore.idl.sstr());
+        new_sstr.distribution = sdist.id;
+        new_sstr._isnew = true;
+        if (!angular.isArray(sdist.streams)){
+            sdist.streams = [];
+        }
+        sdist.streams.push(new_sstr);
+    }
+
+    $scope.abort_changes = function(form) {
+        reload();
+        form.$setPristine();
+    }
+    $scope.save_subscriptions = function(form) {
+        // traverse through structure and set _ischanged
+        // TODO add more granular dirty input detection
+        angular.forEach($scope.subscriptions, function(ssub) {
+            if (!ssub._isnew) ssub._ischanged = true;
+            angular.forEach(ssub.distributions, function(sdist) {
+                if (!sdist._isnew) sdist._ischanged = true;
+                angular.forEach(sdist.streams, function(sstr) {
+                    if (!sstr._isnew) sstr._ischanged = true;
+                });
+            });
+        });
+
+        var obj = egCore.idl.fromTypedHash($scope.subscriptions);
+
+        // create a bunch of promises that each get resolved upon each
+        // CUD update; that way, we can know when the entire save
+        // operation is completed
+        var promises = [];
+        angular.forEach(obj, function(ssub) {
+            ssub._cud_done = $q.defer();
+            promises.push(ssub._cud_done.promise);
+            angular.forEach(ssub.distributions(), function(sdist) {
+                sdist._cud_done = $q.defer();
+                promises.push(sdist._cud_done.promise);
+                angular.forEach(sdist.streams(), function(sstr) {
+                    sstr._cud_done = $q.defer();
+                    promises.push(sstr._cud_done.promise);
+                });
+            });
+        });
+
+        angular.forEach(obj, function(ssub) {
+            ssub.owning_lib(ssub.owning_lib().id()); // deflesh
+            egCore.pcrud.apply(ssub).then(function(res) {
+                var ssub_id = (ssub.isnew() && angular.isObject(res)) ? res.id() : ssub.id();
+                angular.forEach(ssub.distributions(), function(sdist) {
+                    // set subscription ID just in case it's new
+                    sdist.holding_lib(sdist.holding_lib().id()); // deflesh
+                    sdist.subscription(ssub_id);
+                    egCore.pcrud.apply(sdist).then(function(res) {
+                        var sdist_id = (sdist.isnew() && angular.isObject(res)) ? res.id() : sdist.id();
+                        angular.forEach(sdist.streams(), function(sstr) {
+                            // set distribution ID just in case it's new
+                            sstr.distribution(sdist_id);
+                            egCore.pcrud.apply(sstr).then(function(res) {
+                                sstr._cud_done.resolve();
+                            });
+                        });
+                    });
+                    sdist._cud_done.resolve();
+                });
+                ssub._cud_done.resolve();
+            });
+        });
+        $q.all(promises).then(function(resolutions) {
+            reload();
+            form.$setPristine();
+        });
+    }
+    $scope.additional_routing = function(rows) {
+        if (!rows) { return; }
+        var row = rows[0];
+        if (!row) { row = $scope.distStreamGridControls.selectedItems()[0]; }
+        if (row && row['sstr.id']) {
+            egCore.pcrud.search('srlu', {
+                    stream : row['sstr.id']
+                }, {
+                    flesh : 2,
+                    flesh_fields : {
+                        'srlu' : ['reader'],
+                        'au'  : ['mailing_address','billing_address','home_ou']
+                    },
+                    order_by : { srlu : 'pos' }
+                },
+                { atomic : true }
+            ).then(function(list) {
+                $uibModal.open({
+                    templateUrl: './serials/t_routing_list',
+                    controller: 'RoutingCtrl',
+                    resolve : {
+                        rowInfo : function() {
+                            return row;
+                        },
+                        routes : function() {
+                            return egCore.idl.toHash(list);
+                        }
+                    }
+                }).result.then(function(routes) {
+                    // delete all of the routes first;
+                    // it's easiest given the constraints
+                    var deletions = [];
+                    var creations = [];
+                    angular.forEach(routes, function(r) {
+                        var srlu = new egCore.idl.srlu();
+                        srlu.stream(r.stream);
+                        srlu.pos(r.pos);
+                        if (r.reader) {
+                            srlu.reader(r.reader.id);
+                        }
+                        srlu.department(r.department);
+                        srlu.note(r.note);
+                        if (r.id) {
+                            srlu.id(r.id);
+                            var srlu_copy = angular.copy(srlu);
+                            srlu_copy.isdeleted(true);
+                            deletions.push(srlu_copy);
+                        }
+                        if (!r.delete_me) {
+                            srlu.isnew(true);
+                            creations.push(srlu);
+                        }
+                    });
+                    egCore.pcrud.apply(deletions.concat(creations)).then(function(){
+                        reload();
+                    });
+                });
+            });
+        }
+    }
+            }]
+    }
+})
+
+.controller('RoutingCtrl',
+       ['$scope','$uibModalInstance','egCore','rowInfo','routes',
+function($scope , $uibModalInstance , egCore , rowInfo , routes ) {
+    $scope.args = {
+         which_radio_button: 'reader'
+        ,reader: ''
+        ,department: ''
+        ,delete_me: false
+    };
+    $scope.stream_id = rowInfo['sstr.id'];
+    $scope.stream_label = rowInfo['sstr.routing_label'];
+    $scope.routes = routes;
+    $scope.readerInFocus = true;
+    $scope.ok = function(count) { $uibModalInstance.close($scope.routes) }
+    $scope.cancel = function () { $uibModalInstance.dismiss() }
+    $scope.model_has_changed = false;
+    $scope.find_user = function () {
+
+        $scope.readerNotFound = null;
+        $scope.reader_obj = null;
+        if (!$scope.args.reader) return;
+
+        egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.get_barcodes',
+            egCore.auth.token(), egCore.auth.user().ws_ou(),
+            'actor', $scope.args.reader)
+
+        .then(function(resp) { // get_barcodes
+
+            if (evt = egCore.evt.parse(resp)) {
+                console.error(evt.toString());
+                return;
+            }
+
+            if (!resp || !resp[0]) {
+                $scope.readerNotFound = $scope.args.reader;
+                return;
+            }
+
+            egCore.pcrud.search('au', {
+                    id : resp[0].id
+                }, {
+                    flesh : 1,
+                    flesh_fields : {
+                        'au'  : ['mailing_address','billing_address','home_ou']
+                    }
+                },
+                { atomic : true }
+            ).then(function(usr) {
+                $scope.reader_obj = egCore.idl.toHash(usr[0]);
+            });
+        });
+    }
+    $scope.add_route = function () {
+        var new_route = {
+             stream: $scope.stream_id
+            ,pos: $scope.routes.length
+            ,note: $scope.args.note
+        }
+        if ($scope.args.which_radio_button == 'reader') {
+            new_route.reader = $scope.reader_obj;
+        } else {
+            new_route.department = $scope.args.department;
+        }
+        $scope.routes.push(new_route);
+        $scope.model_has_changed = true;
+    }
+    function adjust_pos_field() {
+        var idx = 0;
+        for (var i = 0; i < $scope.routes.length; i++) {
+            $scope.routes[i].pos = $scope.routes[i].delete_me ? idx : idx++;
+        }
+        $scope.model_has_changed = true;
+    }
+    $scope.move_route_up = function(r) {
+        var pos = r.pos;
+        if (pos > 0) {
+            var temp = $scope.routes[ pos - 1 ];
+            $scope.routes[ pos - 1 ] = $scope.routes[ pos ];
+            $scope.routes[ pos ] = temp;
+            adjust_pos_field();
+        }
+    }
+    $scope.move_route_down = function(r) {
+        var pos = r.pos;
+        if (pos < $scope.routes.length - 1) {
+            var temp = $scope.routes[ pos + 1 ];
+            $scope.routes[ pos + 1 ] = $scope.routes[ pos ];
+            $scope.routes[ pos ] = temp;
+            adjust_pos_field();
+        }
+    }
+    $scope.toggle_delete = function(r) {
+        r.delete_me = ! r.delete_me;
+        adjust_pos_field();
+    }
+    $scope.$watch("args.reader", function(newVal, oldVal) {
+        if (newVal && newVal != oldVal) {
+            $scope.find_user();
+        }
+    });
+}])