grid field wildcards; billing cont.
authorBill Erickson <berick@esilibrary.com>
Tue, 27 May 2014 15:02:54 +0000 (11:02 -0400)
committerBill Erickson <berick@esilibrary.com>
Tue, 27 May 2014 15:02:54 +0000 (11:02 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
Open-ILS/src/templates/staff/circ/patron/t_bills_list.tt2
Open-ILS/src/templates/staff/css/style.css.tt2
Open-ILS/src/templates/staff/share/t_autogrid.tt2
Open-ILS/web/js/ui/default/staff/circ/patron/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/bills.js
Open-ILS/web/js/ui/default/staff/services/grid.js

index 89d6252..eb66017 100644 (file)
   <!-- virtual field -->
   <eg-grid-field datatype="money" label="[% ('Payment Pending') %]" 
     name="payment_pending"></eg-grid-field>
+
+  <!-- import all circ fields, hidden by default -->
+  <eg-grid-field path='circulation.*' hidden> </eg-grid-field>
+
+  <eg-grid-field path='circulation.target_copy.*' hidden> </eg-grid-field>
   
 </eg-grid>
  
index e848693..8b51943 100644 (file)
 /* status bar along the bottom of the page ------------------------ */
 /* decrease padding to decrease overall height */
 
+/** TODO:move status bar items into navbar config entry (top-right)
+ * to avoid body padding weirdness.  Or if we want a permenently
+ * visible status bar, maybe put it just below the navbar.. */
+
 /* bottom padding ensures no body content is hidden behind the status
  * bar.  When content reaches the status bar a scroll bar appears */
-body { padding-bottom: 26px; }
+/*body { padding-bottom: 26px; }*/
 
 #status-bar {
   min-height:1.8em !important;
@@ -324,6 +328,13 @@ table.list tr.selected td {
   height: 600px; 
 }
 */
+.eg-grid-column-picker {
+  height: auto;
+  max-height: 400px;
+  overflow: auto;
+  box-shadow: none;
+}
+
 
 /* ----------------------------------------------------------------------
  * /Grid
index 018d6de..883d3dd 100644 (file)
@@ -8,7 +8,8 @@
 
   <div class="eg-grid-primary-label">{{mainLabel}}</div>
 
-  <div class="btn-group eg-grid-menuiitem" ng-if="menuLabel" dropdown>
+  <div class="btn-group eg-grid-menuiitem" 
+    is-open="gridMenuIsOpen" ng-if="menuLabel" dropdown>
     <button type="button" class="btn btn-default dropdown-toggle">
       {{menuLabel}}<span class="caret"></span>
     </button>
     </button>
 
     <!-- actions drop-down menu -->
-    <div class="btn-group" ng-if="actions.length" dropdown>                                                  
+    <div class="btn-group" ng-if="actions.length" 
+      is-open="gridActionsIsOpen" dropdown>                                                  
       <button type="button" class="btn btn-default dropdown-toggle"
         ng-class="{disabled : false}">
         [% l('Actions') %] <span class="caret"></span>                       
       </button>                                                              
       <ul class="dropdown-menu pull-right">                                  
         <li ng-class="{disabled : false}" ng-repeat="action in actions">
-          <a href="" ng-click="actionLauncher(action)">{{action.label}}</a>
+          <a href ng-click="actionLauncher(action)">{{action.label}}</a>
         </li>                                                                
       </ul>
     </div>
 
-    <div class="btn-group" dropdown>
+    <div class="btn-group" dropdown is-open="gridRowCountIsOpen">
       <button type="button" title="[% ('Select Row Count') %]"
         class="btn btn-default dropdown-toggle">
         [% l('Rows [_1]', '{{limit()}}') %]
       </button>
       <ul class="dropdown-menu">
         <li ng-repeat="t in [5,10,25,50,100]">
-          <a href dropdown-toggle ng-click='offset(0);limit(t);collect()'>
+          <a href ng-click='offset(0);limit(t);collect()'>
             {{t}}
           </a>
         </li>
       </ul>
     </div>
 
-    <div class="btn-group" dropdown>
+    <div class="btn-group" dropdown is-open="gridPageSelectIsOpen">
       <button type="button" title="[% ('Select Page') %]"
         class="btn btn-default dropdown-toggle">
         [% l('Page [_1]', '{{page()}}') %]
               ng-click="$event.stopPropagation()"/>
             <span class="input-group-btn">
               <button class="btn btn-default" type="button"
-                ng-click="goToPage(pageFromUI);pageFromUI=''">
+                ng-click="goToPage(pageFromUI);pageFromUI='';">
                 [% l('Go To...') %]
               </button>
             </span>
         </li>
         <li role="presentation" class="divider"></li>
         <li ng-repeat="t in [1,2,3,4,5,10,25,50,100]">
-          <a href dropdown-toggle ng-click='goToPage(t)'>{{t}}</a>
+          <a href ng-click='goToPage(t);gridPageSelectIsOpen=false;'>{{t}}</a>
         </li>
       </ul>
     </div>
 
-    <div class="btn-group" dropdown>
-    <button type="button" 
-      class="btn btn-default dropdown-toggle">
-      <span class="caret"></span>
-    </button>
-    <ul class="dropdown-menu pull-right">
-      <li><a href dropdown-toggle ng-click="toggleConfDisplay()">
-        <span class="glyphicon glyphicon-wrench"></span>
-        [% l('Configure Columns') %]
-      </a></li>
-      <li><a href dropdown-toggle ng-click="saveConfig()">
-        <span class="glyphicon glyphicon-floppy-save"></span>
-        [% l('Save Columns') %]
-      </a></li>
-      <li><a href dropdown-toggle ng-click="showAllColumns()">
-        <span class="glyphicon glyphicon-resize-full"></span>
-        [% l('Show All Columns') %]
-      </a></li>
-      <li><a href dropdown-toggle ng-click="hideAllColumns()">
-        <span class="glyphicon glyphicon-resize-small"></span>
-        [% l('Hide All Columns') %]
-      </a></li>
-      <li><a ng-click="generateCSVExportURL()" 
-        download="{{csvExportFileName}}.csv" ng-href="{{csvExportURL}}">
-        <span class="glyphicon glyphicon-download"></span>
-        [% l('Download CSV') %]
-      </a></li>
-      <li><a href dropdown-toggle ng-click="printCSV()">
-        <span class="glyphicon glyphicon-print"></span>
-        [% l('Print CSV') %]
-      </a></li>
-      <li role="presentation" class="divider"></li>
-      <li ng-repeat="col in columns">
-        <a href dropdown-toggle ng-click="col.visible = !col.visible">
-            <span ng-if="col.visible" 
-              class="label label-success">&#x2713;</span>
-            <span ng-if="!col.visible" 
-              class="label label-warning">&#x2717;</span>
-            <span>{{col.label}}</span>
-        </a>
-      </li>
-    </ul>
+    <div class="btn-group" dropdown is-open="gridColumnPickerIsOpen">
+      <button type="button" 
+        class="btn btn-default dropdown-toggle">
+        <span class="caret"></span>
+      </button>
+      <ul class="dropdown-menu pull-right eg-grid-column-picker">
+        <li><a href ng-click="toggleConfDisplay()">
+          <span class="glyphicon glyphicon-wrench"></span>
+          [% l('Configure Columns') %]
+        </a></li>
+        <li><a href ng-click="saveConfig()">
+          <span class="glyphicon glyphicon-floppy-save"></span>
+          [% l('Save Columns') %]
+        </a></li>
+        <li><a href ng-click="showAllColumns()">
+          <span class="glyphicon glyphicon-resize-full"></span>
+          [% l('Show All Columns') %]
+        </a></li>
+        <li><a href ng-click="hideAllColumns()">
+          <span class="glyphicon glyphicon-resize-small"></span>
+          [% l('Hide All Columns') %]
+        </a></li>
+        <li><a href ng-click="resetColumns()">
+          <span class="glyphicon glyphicon-refresh"></span>
+          [% l('Reset Columns') %]
+        </a></li>
+        <li><a ng-click="generateCSVExportURL()" 
+          download="{{csvExportFileName}}.csv" ng-href="{{csvExportURL}}">
+          <span class="glyphicon glyphicon-download"></span>
+          [% l('Download CSV') %]
+        </a></li>
+        <li><a href ng-click="printCSV()">
+          <span class="glyphicon glyphicon-print"></span>
+          [% l('Print CSV') %]
+        </a></li>
+        <li role="presentation" class="divider"></li>
+        <li ng-repeat="col in columns">
+          <a href ng-click="toggleColumnVisibility(col)">
+              <span ng-if="col.visible" 
+                class="label label-success">&#x2713;</span>
+              <span ng-if="!col.visible" 
+                class="label label-warning">&#x2717;</span>
+              <span>{{col.label}}</span>
+          </a>
+        </li>
+      </ul>
     </div>
   </div>
 </div>
index 0020d9f..861cf9a 100644 (file)
@@ -280,7 +280,7 @@ function($q , $timeout , $location , egCore,  egUser) {
     service.fetchUserStats = function() {
         return egCore.net.request(
             'open-ils.actor',
-            'open-ils.actor.user.opac.vital_stats', 
+            'open-ils.actor.user.opac.vital_stats.authoritative', 
             egCore.auth.token(), service.current.id()
         ).then(
             function(stats) {
index 8aefec4..0e6b2fb 100644 (file)
@@ -238,6 +238,7 @@ function($scope,  $q , $routeParams,  $locale , egCore , egGridDataProvider , bi
         billSvc.applyPayment(
             $scope.payment_type, generatePayments(), note)
         .then(function() {
+            patronSvc.fetchUserStats();
             billSvc.fetchSummary().then(function(s) {$scope.summary = s});
             $scope.payment_amount = 0;
             $scope.gridRevision++; // tell the grid to refresh itself
@@ -271,6 +272,7 @@ function($scope,  $q , $routeParams,  $locale , egCore , egGridDataProvider , bi
                 // send the billing to the server using the arguments
                 // provided in the billing dialog, then refresh
                 billSvc.billPatron(args).then(function() {
+                    patronSvc.fetchUserStats();
                     $scope.payment_amount = 0;
                     $scope.gridRevision++;
                 });
index 7a9f147..2c816fb 100644 (file)
@@ -92,9 +92,9 @@ angular.module('egGridMod',
         },
 
         controller : [
-                    '$scope','$q','egCore','egGridFlatDataProvider',
+                    '$scope','$q','egCore','egGridFlatDataProvider','$location',
                     'egGridColumnsProvider','$filter','$window','$sce',
-            function($scope,  $q , egCore,  egGridFlatDataProvider,  
+            function($scope,  $q , egCore,  egGridFlatDataProvider , $location,
                      egGridColumnsProvider , $filter , $window , $sce) {
 
             var grid = this;
@@ -117,10 +117,11 @@ angular.module('egGridMod',
                 grid.addMenuItem = function(item) {
                     $scope.menuItems.push(item);
                     var handler = item.handler;
-                    if (handler) {
-                        item.handler = function() {
-                            handler(item, item.handlerData, 
-                                grid.getSelectedItems());
+                    item.handler = function() {
+                        $scope.gridMenuIsOpen = false; // close menu
+                        if (handler) {
+                            handler(item, 
+                                item.handlerData, grid.getSelectedItems());
                         }
                     }
                 }
@@ -156,10 +157,21 @@ angular.module('egGridMod',
                 $scope.showAllColumns = function() {
                     grid.columnsProvider.showAllColumns();
                 }
+
                 $scope.hideAllColumns = function() {
                     grid.columnsProvider.hideAllColumns();
                 }
 
+                $scope.toggleColumnVisibility = function(col) {
+                    $scope.gridColumnPickerIsOpen = false;
+                    col.visible = !col.visible;
+
+                    // egGridFlatDataProvider only retrieves data to be
+                    // displayed.  When column visibility changes, it's
+                    // necessary to fetch the newly visible column data.
+                    if (grid.selfManagedData) grid.collect();
+                }
+
                 if (!grid.indexField && grid.idlClass)
                     grid.indexField = egCore.idl.classes[grid.idlClass].pkey;
 
@@ -248,9 +260,23 @@ angular.module('egGridMod',
                 });
             }
 
+            // remove the stored column configuration preferenc, then recover 
+            // the column visibility information from the initial page load.
+            $scope.resetColumns = function() {
+                $scope.gridColumnPickerIsOpen = false;
+                egCore.hatch.removeItem('eg.grid.' + grid.persistKey)
+                .then(function() {
+                    grid.columnsProvider.reset(); 
+                    if (grid.selfManagedData) grid.collect();
+                });
+            }
+
+
             // save the columns configuration (position, sort, width) to
             // eg.grid.<persist-key>
             $scope.saveConfig = function() {
+                $scope.gridColumnPickerIsOpen = false;
+
                 if (!grid.persistKey) {
                     console.warn(
                         "Cannot save settings without a grid persist-key");
@@ -404,6 +430,7 @@ angular.module('egGridMod',
 
             // fires the action handler function
             $scope.actionLauncher = function(action) {
+                $scope.gridActionsIsOpen = false;
                 action.handler(grid.getSelectedItems());
             }
 
@@ -619,6 +646,8 @@ angular.module('egGridMod',
                 } else {
                     $scope.showGridConf = true;
                 }
+
+                $scope.gridColumnPickerIsOpen = false;
             }
 
             // called when a dragged column is dropped onto itself
@@ -664,6 +693,7 @@ angular.module('egGridMod',
             // sets the download file name and inserts the current CSV
             // into a Blob URL for browser download.
             $scope.generateCSVExportURL = function() {
+                $scope.gridColumnPickerIsOpen = false;
 
                 // let the file name describe the grid
                 $scope.csvExportFileName = 
@@ -678,6 +708,7 @@ angular.module('egGridMod',
             }
 
             $scope.printCSV = function() {
+                $scope.gridColumnPickerIsOpen = false;
                 egCore.hatch.print('text/plain', grid.generateCSV())
                 .then(function() { console.debug('print complete') });
             }
@@ -738,6 +769,12 @@ angular.module('egGridMod',
             // asks the dataProvider for a page of data
             grid.collect = function() {
                 if (grid.collecting) return; // avoid parallel collects()
+
+                // ensure all of our dropdowns are closed
+                $scope.gridColumnPickerIsOpen = false;
+                $scope.gridRowCountIsOpen = false;
+                $scope.gridPageSelectIsOpen = false;
+
                 $scope.items = [];
                 $scope.selected = {};
                 grid.collecting = true;
@@ -800,7 +837,6 @@ angular.module('egGridMod',
             if (tmpl && !tmpl.match(/^\s*$/))
                 scope.template = tmpl
 
-               'datatype', // for non-IDL fields, can be used to apply filters
             egGridCtrl.columnsProvider.add(scope);
             scope.$destroy();
         }
@@ -835,11 +871,27 @@ angular.module('egGridMod',
     function ColumnsProvider(args) {
         var cols = this;
         cols.columns = [];
+        cols.stockVisible = [];
         cols.idlClass = args.idlClass;
         cols.defaultToHidden = args.defaultToHidden;
         cols.defaultToNoSort = args.defaultToNoSort;
         cols.defaultToNoMultiSort = args.defaultToNoMultiSort;
 
+        // resets column width, visibility, and sort behavior
+        // Visibility resets to the visibility settings defined in the 
+        // template (i.e. the original egGridField values).
+        cols.reset = function() {
+            angular.forEach(cols.columns, function(col) {
+                col.flex = 2;
+                col.sort = 0;
+                if (cols.stockVisible.indexOf(col.name) > -1) {
+                    col.visible = true;
+                } else {
+                    col.visible = false;
+                }
+            });
+        }
+
         // returns true if any columns are sortable
         cols.hasSortableColumn = function() {
             return cols.columns.filter(
@@ -850,15 +902,19 @@ angular.module('egGridMod',
         }
 
         cols.showAllColumns = function() {
+            $scope.gridColumnPickerIsOpen = false;
             angular.forEach(cols.columns, function(column) {
                 column.visible = true;
             });
+            if (grid.selfManagedData) grid.collect();
         }
 
         cols.hideAllColumns = function() {
+            $scope.gridColumnPickerIsOpen = false;
             angular.forEach(cols.columns, function(col) {
                 delete col.visible;
             });
+            // note: no need to fetch new data if no columns are visible
         }
 
         cols.indexOf = function(name) {
@@ -900,12 +956,62 @@ angular.module('egGridMod',
             );
         }
 
-        // Add a column to the columns collection.
-        // Columns may come from a slim eg-columns-field or 
-        // directly from the IDL.
-        cols.add = function(colSpec, fromIDL) {
+        // if a column definition has a path with a wildcard, create
+        // columns for all non-virtual fields at the specified 
+        // position in the path.
+        cols.expandPath = function(colSpec) {
+
+            var dotpath = colSpec.path.replace(/\.\*$/,'');
+            var class_obj = egCore.idl.classes[cols.idlClass];
+            var path_parts = dotpath.split(/\./);
+
+            // find the IDL class definition for the last element in the
+            // path before the .*
+            var class_obj;
+            for (var path_idx in path_parts) {
+                var part = path_parts[path_idx];
+                var idl_field = class_obj.field_map[part];
 
-            var column = {
+                // unless we're at the end of the list, this field should
+                // link to another class.
+                if (idl_field && idl_field['class'] && (
+                    idl_field.datatype == 'link' || 
+                    idl_field.datatype == 'org_unit')) {
+                    class_obj = egCore.idl.classes[idl_field['class']];
+                } else {
+                    if (path_idx < (path_parts.length - 1)) {
+                        // we ran out of classes to hop through before
+                        // we ran out of path components
+                        console.error("egGrid: invalid IDL path: " + path);
+                    }
+                }
+            }
+
+            if (class_obj) {
+                angular.forEach(class_obj.fields, function(field) {
+
+                    // Only show wildcard fields where we have data to show
+                    // Virtual and un-fleshed links will not have any data.
+                    if (field.virtual || (
+                        field.datatype == 'link' || field.datatype == 'org_unit'))
+                        return;
+
+                    var col = cols.cloneFromScope(colSpec);
+                    col.path = dotpath + '.' + field.name;
+                    cols.add(col, null, true);
+                });
+
+            } else {
+                console.error(
+                    "egGrid: wildcard path does not resolve to an object: "
+                    + dotpath);
+            }
+        }
+
+        // angular.clone(scopeObject) is not permittable.  Manually copy
+        // the fields over that we need (so the scope object can go away).
+        cols.cloneFromScope = function(colSpec) {
+            return {
                 name  : colSpec.name,
                 label : colSpec.label,
                 path  : colSpec.path,
@@ -922,6 +1028,27 @@ angular.module('egGridMod',
                 multisortable    : colSpec.multisortable,
                 nonmultisortable : colSpec.nonmultisortable
             };
+        }
+
+
+        // Add a column to the columns collection.
+        // Columns may come from a slim eg-columns-field or 
+        // directly from the IDL.
+        cols.add = function(colSpec, fromIDL, fromExpand) {
+
+            // First added column with the specified path takes precedence.
+            // This allows for specific definitions followed by wildcard
+            // definitions.  If a match is found, back out.
+            if (cols.columns.filter(function(c) {
+                return (c.path == colSpec.path) })[0]) {
+                console.debug('skipping column ' + colSpec.path);
+                return;
+            }
+
+            var column = fromExpand ? colSpec : cols.cloneFromScope(colSpec);
+
+            if (column.path && column.path.match(/\*/)) 
+                return cols.expandPath(colSpec);
 
             if (!column.name) column.name = column.path;
             if (!column.path) column.path = column.name;
@@ -938,22 +1065,32 @@ angular.module('egGridMod',
 
             cols.columns.push(column);
 
+            // Track which columns are visible by default in case we
+            // need to reset column visibility
+            if (column.visible) 
+                cols.stockVisible.push(column.name);
+
             if (fromIDL) return;
             if (!cols.idlClass) return; // ad-hoc object
 
             // lookup the matching IDL field
-            var idl_field = cols.idlFieldFromPath(column.path);
+            var idl_info = cols.idlFieldFromPath(column.path);
 
-            if (!idl_field) {
+            if (!idl_info) {
                 // column is not represented within the IDL
                 column.adhoc = true; 
                 return; 
             }
 
-            column.datatype = idl_field.datatype;
+            column.datatype = idl_info.idl_field.datatype;
             
             if (!column.label) {
-                column.label = idl_field.label || column.name;
+                column.label = idl_info.idl_field.label || column.name;
+                if (fromExpand) {
+                    var label = 
+                        idl_info.idl_class.label || idl_info.idl_class.name;
+                    column.label = label + '::' + column.label;
+                }
             }
         },
 
@@ -963,21 +1100,10 @@ angular.module('egGridMod',
             var class_obj = egCore.idl.classes[cols.idlClass];
             var path_parts = dotpath.split(/\./);
 
-            // for() == early exit
             var idl_field;
             for (var path_idx in path_parts) {
                 var part = path_parts[path_idx];
-
-                // find the field object matching the path component
-                for (var field_idx in class_obj.fields) {
-                    if (class_obj.fields[field_idx].name == part) {
-                        idl_field = class_obj.fields[field_idx];
-                        break;
-                    }
-                }
-
-                // unless we're at the end of the list, this field should
-                // link to another class.
+                idl_field = class_obj.field_map[part];
 
                 if (idl_field && idl_field['class'] && (
                     idl_field.datatype == 'link' || 
@@ -992,7 +1118,12 @@ angular.module('egGridMod',
                 }
             }
 
-            return idl_field;
+            if (!idl_field) return null;
+
+            return {
+                idl_field :idl_field,
+                idl_class : class_obj
+            };
         }
     }