lpxxx Angular holdings maintenance wip
authorBill Erickson <berickxx@gmail.com>
Tue, 19 Mar 2019 15:48:03 +0000 (11:48 -0400)
committerBill Erickson <berickxx@gmail.com>
Tue, 19 Mar 2019 15:48:03 +0000 (11:48 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html
Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts
Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html

index 64f36e5..bf52735 100644 (file)
@@ -1,4 +1,6 @@
 
+
+<!-- Location / Barcode cell template -->
 <ng-template #locationTemplate let-row="row" let-userContext="userContext">
   <!-- pl-* is doubled for added impact -->
   <div class="pl-{{row.locationDepth}}">
@@ -18,6 +20,7 @@
   </div>
 </ng-template>
 
+<!-- TODO: create a generic yes/no template -->
 <ng-template #holdableTemplate let-row="row" let-userContext="userContext">
   <ng-container *ngIf="row.copy">
     <ng-container *ngIf="userContext.copyIsHoldable(row.copy); else notHoldable">
@@ -30,6 +33,7 @@
 <eg-mark-damaged-dialog #markDamagedDialog></eg-mark-damaged-dialog>
 <eg-mark-missing-dialog #markMissingDialog></eg-mark-missing-dialog>
 
+<!-- holdings grid -->
 <div class='eg-copies w-100 mt-3'>
   <eg-grid #holdingsGrid [dataSource]="gridDataSource"
     (onRowActivate)="onRowActivate($event)" [disablePaging]="true"
     </eg-grid-toolbar-action>
 
     <!-- fields -->
+    <!-- NOTE column names were added to match the names from the AngJS grid
+        so grid settings would propagate -->
 
     <eg-grid-column path="index" [hidden]="true" [index]="true">
     </eg-grid-column>
-    <eg-grid-column path="copy.id" [hidden]="true" label="Copy ID" i18n-label>
+    <eg-grid-column name="id" path="copy.id" [hidden]="true" label="Copy ID" i18n-label>
     </eg-grid-column>
     <eg-grid-column path="volume.id" [hidden]="true" label="Volume ID" i18n-label>
     </eg-grid-column>
-    <eg-grid-column name="location_barcode" [flex]="4"
+    <eg-grid-column name="owner_label" [flex]="4"
       [cellTemplate]="locationTemplate" [cellContext]="gridTemplateContext" 
       label="Location/Barcode" [disableTooltip]="true" i18n-label>
     </eg-grid-column>
     </eg-grid-column>
     <eg-grid-column path="copyCount" datatype="number" label="Copies" i18n-label>
     </eg-grid-column>
-    <eg-grid-column path="callNumberLabel" label="Call Number" i18n-label>
+    <eg-grid-column path="volume._label" name="call_number.label" label="Call Number" i18n-label>
+    </eg-grid-column>
+    <eg-grid-column path="copy.barcode" name="barcode" label="Barcode" i18n-label>
     </eg-grid-column>
     <eg-grid-column i18n-label label="Circ Library" path="copy.circ_lib" 
-      datatype="org_unit"></eg-grid-column>
-    <eg-grid-column i18n-label label="Owning Library" path="volume.owning_lib" 
+      name="circ_lib.name" datatype="org_unit"></eg-grid-column>
+    <eg-grid-column i18n-label label="Owning Library" name="owner_label" path="volume.owning_lib" 
       datatype="org_unit"></eg-grid-column>
     <eg-grid-column i18n-label label="Due Date" path="circ.due_date" 
       datatype="timestamp"></eg-grid-column>
-    <eg-grid-column i18n-label label="Shelving Location" path="copy.location.name">
+    <eg-grid-column i18n-label label="Shelving Location" path="copy.location.name" name="location.name">
     </eg-grid-column>
-    <eg-grid-column i18n-label label="Circulation Modifier" path="copy.circ_modifier">
+    <eg-grid-column i18n-label label="Circulation Modifier" 
+      path="copy.circ_modifier" name="circ_modifier">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Copy Number" path="copy.copy_number" name="copy_number" [hidden]="true">
     </eg-grid-column>
 
-    <eg-grid-column i18n-label label="Status" path="copy.status.name">
+    <eg-grid-column i18n-label label="Status" path="copy.status.name" name="status_name">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Call Number Prefix" 
+      path="volume.prefix.label" name="call_number.prefix.label" [hidden]="true">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Call Number Suffix" 
+      path="volume.suffix.label" name="call_number.suffix.label" [hidden]="true">
     </eg-grid-column>
     <eg-grid-column i18n-label label="Active/Create Date" 
       path="copy.active_date" datatype="timestamp">
     </eg-grid-column>
     <eg-grid-column i18n-label label="Age Hold Protection" 
-      path="copy.age_protect.name"></eg-grid-column>
+      path="copy.age_protect.name" name="age_protect.name"></eg-grid-column>
+    <eg-grid-column i18n-label label="Copy Price" 
+      path="copy.price" name="price" [hidden]="true"></eg-grid-column>
 
+    <eg-grid-column i18n-label label="Circulate" path="copy.circualte" 
+      name="circulate" datatype="bool" [hidden]="true"></eg-grid-column>
+    <eg-grid-column i18n-label label="Deposit" path="copy.deposit" 
+      name="deposit" datatype="bool" [hidden]="true"></eg-grid-column>
+    <eg-grid-column i18n-label label="Deposit Amount" path="copy.deposit_amount" 
+      name="deposit_amount" datatype="money" [hidden]="true"></eg-grid-column>
     <eg-grid-column i18n-label label="Holdable?" name="holdable" 
       [cellTemplate]="holdableTemplate" [cellContext]="gridTemplateContext">
     </eg-grid-column>
-
+    <eg-grid-column i18n-label label="Reference?" path="copy.ref" 
+      name="ref" datatype="bool" [hidden]="true"></eg-grid-column>
+    <eg-grid-column i18n-label label="Last Inventory Date" 
+      path="copy.latest_inventory.inventory_date" 
+      name="latest_inventory.inventory_date" datatype="timestamp" [hidden]="true">
+    </eg-grid-column>
+    <eg-grid-column i18n-label label="Last Inventory Workstation" 
+      path="copy.latest_inventory.inventory_workstation.name" 
+      name="latest_inventory.inventory_workstation.name" [hidden]="true">
+    </eg-grid-column>
   </eg-grid>
 </div>
 
index 3bb2efa..2ced83b 100644 (file)
@@ -61,7 +61,6 @@ class HoldingsEntry {
 })
 export class HoldingsMaintenanceComponent implements OnInit {
 
-    recId: number;
     initDone = false;
     gridDataSource: GridDataSource;
     gridTemplateContext: any;
@@ -81,10 +80,17 @@ export class HoldingsMaintenanceComponent implements OnInit {
     @ViewChild('markMissingDialog')
         private markMissingDialog: MarkMissingDialogComponent;
 
-    contextOrg: IdlObject;
+
     holdingsTree: HoldingsTree;
-    holdingsTreeOrgCache: {[id: number]: HoldingsTreeNode};
+
+    // nodeType => id => tree node cache
+    treeNodeCache: {[nodeType: string]: {[id: number]: HoldingsTreeNode}};
+
+    // When true and a grid reload is called, the holdings data will be
+    // re-fetched from the server.
     refreshHoldings: boolean;
+
+    // Used as a row identifier in th grid, since we're mixing object types.
     gridIndex: number;
 
     // List of copies whose due date we need to retrieve.
@@ -94,17 +100,33 @@ export class HoldingsMaintenanceComponent implements OnInit {
     // When not true, render based on the current "expanded" state of each node.
     // Rendering from prefs happens on initial load and when any prefs change.
     renderFromPrefs: boolean;
+
     rowClassCallback: (row: any) => string;
 
+    private _recId: number;
     @Input() set recordId(id: number) {
-        this.recId = id;
+        this._recId = id;
         // Only force new data collection when recordId()
         // is invoked after ngInit() has already run.
         if (this.initDone) {
-            this.refreshHoldings = true;
-            this.holdingsGrid.reload();
+            this.hardRefresh();
+        }
+    }
+    get recordId(): number {
+        return this._recId;
+    }
+
+    // Allows the caller to update the context org unit
+    private _co: IdlObject;
+    @Input() set contextOrg(org: IdlObject) {
+        this._co = org;
+        if (this.initDone) {
+            this.hardRefresh();
         }
     }
+    get contextOrg(): IdlObject {
+        return this._co;
+    }
 
     constructor(
         private net: NetService,
@@ -115,7 +137,6 @@ export class HoldingsMaintenanceComponent implements OnInit {
         private store: ServerStoreService
     ) {
         // Set some sane defaults before settings are loaded.
-        this.contextOrg = this.org.get(this.auth.user().ws_ou());
         this.gridDataSource = new GridDataSource();
         this.refreshHoldings = true;
         this.renderFromPrefs = true;
@@ -154,6 +175,10 @@ export class HoldingsMaintenanceComponent implements OnInit {
     ngOnInit() {
         this.initDone = true;
 
+        if (!this.contextOrg) {
+            this.contextOrg = this.org.get(this.auth.user().ws_ou());
+        }
+
         // These are pre-cached via the resolver.
         const settings = this.store.getItemBatchCached([
             'cat.holdings_show_empty_org',
@@ -173,8 +198,11 @@ export class HoldingsMaintenanceComponent implements OnInit {
         };
     }
 
-    ngAfterViewInit() {
-
+    hardRefresh() {
+        this.renderFromPrefs = true;
+        this.refreshHoldings = true;
+        this.initHoldingsTree();
+        this.holdingsGrid.reload();
     }
 
     toggleShowCopies(value: boolean) {
@@ -223,33 +251,40 @@ export class HoldingsMaintenanceComponent implements OnInit {
 
     initHoldingsTree() {
 
+        const visibleOrgs = this.org.fullPath(this.contextOrg, true);
+
         // The initial tree simply matches the org unit tree
         const traverseOrg = (node: HoldingsTreeNode) => {
-            node.expanded = true;
             node.target.children().forEach((org: IdlObject) => {
+                if (visibleOrgs.indexOf(org.id()) == -1) {
+                    return; // Org is outside of scope
+                }
                 const nodeChild = new HoldingsTreeNode();
                 nodeChild.nodeType = 'org';
                 nodeChild.target = org;
                 nodeChild.parentNode = node;
                 node.children.push(nodeChild);
-                this.holdingsTreeOrgCache[org.id()] = nodeChild;
+                this.treeNodeCache.org[org.id()] = nodeChild;
                 traverseOrg(nodeChild);
             });
         }
 
+        this.treeNodeCache = {
+            org: {},
+            volume: {},
+            copy: {}
+        };
+
         this.holdingsTree = new HoldingsTree();
         this.holdingsTree.root.nodeType = 'org';
         this.holdingsTree.root.target = this.org.root();
-
-        this.holdingsTreeOrgCache = {};
-        this.holdingsTreeOrgCache[this.org.root().id()] = this.holdingsTree.root;
+        this.treeNodeCache.org[this.org.root().id()] = this.holdingsTree.root;
 
         traverseOrg(this.holdingsTree.root);
     }
 
     // Org node children are sorted with any child org nodes pushed to the
     // front, followed by the call number nodes sorted alphabetcially by label.
-    // TODO: prefix/suffix
     sortOrgNodeChildren(node: HoldingsTreeNode) {
         node.children = node.children.sort((a, b) => {
             if (a.nodeType === 'org') {
@@ -261,7 +296,9 @@ export class HoldingsMaintenanceComponent implements OnInit {
             } else if (b.nodeType === 'org') {
                 return 1;
             } else {
-                return a.target.label() < b.target.label() ? -1 : 1;
+                // TODO: should this use label sortkey instead of
+                // the compiled volume label?
+                return a.target._label < b.target._label ? -1 : 1;
             }
         });
     }
@@ -322,7 +359,7 @@ export class HoldingsMaintenanceComponent implements OnInit {
 
         switch(node.nodeType) {
             case 'org':
-                if (this.renderFromPrefs && node.volumeCount === 0
+                if (node.volumeCount === 0
                     && !this.emptyLibsCheckbox.checked()) {
                     return;
                 }
@@ -334,7 +371,16 @@ export class HoldingsMaintenanceComponent implements OnInit {
                 break;
 
             case 'volume':
-                entry.locationLabel = node.target.label(); // TODO prefix/suffix
+                if (this.renderFromPrefs) {
+                    if (!this.volsCheckbox.checked()) {
+                        return;
+                    }
+                    if (node.copyCount === 0
+                        && !this.emptyVolsCheckbox.checked()) {
+                        return;
+                    }
+                }
+                entry.locationLabel = node.target._label;
                 entry.locationDepth = node.parentNode.target.ou_type().depth() + 1;
                 entry.callNumberLabel = entry.locationLabel;
                 entry.volume = node.target;
@@ -370,29 +416,9 @@ export class HoldingsMaintenanceComponent implements OnInit {
         this.renderFromPrefs = false;
     }
 
-    // Find an existing tree node by id and type
-    findNode(targetId: number, nodeType: string): HoldingsTreeNode {
-        const id = Number(targetId);
-
-        const search = (node: HoldingsTreeNode): HoldingsTreeNode => {
-            if (!node) return null;
-
-            if (node.nodeType === nodeType && Number(node.target.id()) === id) {
-                return node;
-            }
-            // for loop for early exit
-            for (let idx = 0; idx < node.children.length; idx++) {
-                const found = search(node.children[idx]);
-                if (found) { return found; }
-            }
-        }
-
-        return search(this.holdingsTree.root);
-    }
-
-
+    // Grab volumes, copies, and related data.
     fetchHoldings(pager: Pager): Observable<any> {
-        if (!this.recId) { return of([]); }
+        if (!this.recordId) { return of([]); }
 
         return new Observable<any>(observer => {
 
@@ -404,8 +430,8 @@ export class HoldingsMaintenanceComponent implements OnInit {
             this.itemCircsNeeded = [];
 
             this.pcrud.search('acn',
-                {   record: this.recId,
-                    owning_lib: this.org.ancestors(this.contextOrg, true),
+                {   record: this.recordId,
+                    owning_lib: this.org.fullPath(this.contextOrg, true),
                     deleted: 'f',
                     label: {'!=' : '##URI##'}
                 }, {
@@ -446,13 +472,23 @@ export class HoldingsMaintenanceComponent implements OnInit {
         })).toPromise();
     }
 
+    // Compile prefix + label + suffix into field volume._label;
+    setVolumeLabel(volume: IdlObject) {
+        const pfx = volume.prefix() ? volume.prefix().label() : '';
+        const sfx = volume.suffix() ? volume.suffix().label() : '';
+        volume._label = pfx ? pfx + ' ' : '';
+        volume._label += volume.label();
+        volume._label += sfx ? ' ' + sfx : '';
+    }
+
     // Create the tree node for the volume if it doesn't already exist.
     // Do the same for its linked copies.
     appendVolume(volume: IdlObject) {
+        let volNode = this.treeNodeCache.volume[volume.id()];
+        this.setVolumeLabel(volume);
 
-        let volNode = this.findNode(volume.id(), 'volume');
         if (volNode) {
-            const pNode = this.holdingsTreeOrgCache[volume.owning_lib()];
+            const pNode = this.treeNodeCache.org[volume.owning_lib()]
             if (volNode.parentNode.target.id() !== pNode.target.id()) {
                 // Volume owning library changed.  Un-link it from the previous
                 // org unit collection before adding to the new one.
@@ -463,8 +499,9 @@ export class HoldingsMaintenanceComponent implements OnInit {
         } else {
             volNode = new HoldingsTreeNode();
             volNode.nodeType = 'volume';
-            volNode.parentNode = this.holdingsTreeOrgCache[volume.owning_lib()];
+            volNode.parentNode = this.treeNodeCache.org[volume.owning_lib()]
             volNode.parentNode.children.push(volNode);
+            this.treeNodeCache.volume[volume.id()] = volNode;
         }
 
         volNode.target = volume;
@@ -474,8 +511,9 @@ export class HoldingsMaintenanceComponent implements OnInit {
             .forEach((copy: IdlObject) => this.appendCopy(volNode, copy));
     }
 
+    // Find or create a copy node.
     appendCopy(volNode: HoldingsTreeNode, copy: IdlObject) {
-        let copyNode = this.findNode(copy.id(), 'copy');
+        let copyNode = this.treeNodeCache.copy[copy.id()];
 
         if (copyNode) {
             const oldParent = copyNode.parentNode;
@@ -491,6 +529,7 @@ export class HoldingsMaintenanceComponent implements OnInit {
             copyNode.nodeType = 'copy';
             volNode.children.push(copyNode);
             copyNode.parentNode = volNode;
+            this.treeNodeCache.copy[copy.id()] = copyNode;
         }
 
         copyNode.target = copy;
@@ -502,6 +541,7 @@ export class HoldingsMaintenanceComponent implements OnInit {
         }
     }
 
+    // Which copies in the grid are selected.
     selectedCopyIds(rows: HoldingsEntry[], skipStatus?: number): number[] {
         let copyRows = rows.filter(r => Boolean(r.copy)).map(r => r.copy);
         if (skipStatus) {
index b583cf7..d43a8bb 100644 (file)
@@ -66,7 +66,8 @@
       </ngb-tab>
       <ngb-tab title="Holdings View" i18n-title id="holdings">
         <ng-template ngbTabContent>
-          <eg-holdings-maintenance [recordId]="recordId">
+          <eg-holdings-maintenance [recordId]="recordId" 
+            [contextOrg]="currentSearchOrg()">
           </eg-holdings-maintenance>
         </ng-template>
       </ngb-tab>