Angular holdings maintenance wip
authorBill Erickson <berickxx@gmail.com>
Mon, 18 Mar 2019 17:49:59 +0000 (13:49 -0400)
committerBill Erickson <berickxx@gmail.com>
Mon, 18 Mar 2019 17:49:59 +0000 (13:49 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/core/server-store.service.ts
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.ts
Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html
Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts

index ea2d93d..b54a4c9 100644 (file)
@@ -60,6 +60,22 @@ export class ServerStoreService {
         );
     }
 
+    // Sync call for items known to be cached locally.
+    getItemCached(key: string): any {
+        return this.cache[key];
+    }
+
+    // Sync batch call for items known to be cached locally
+    getItemBatchCached(keys: string[]): {[key: string]: any} {
+        const values: any = {};
+        keys.forEach(key => {
+            if (key in this.cache) {
+                values[key] = this.cache[key];
+            }
+        });
+        return values;
+    }
+
     // Returns a set of key/value pairs for the requested settings
     getItemBatch(keys: string[]): Promise<any> {
 
index 7ee3019..24b7616 100644 (file)
@@ -16,29 +16,48 @@ export class GridToolbarCheckboxComponent implements OnInit {
     // This does NOT fire the onChange handler.
     @Input() initialValue: boolean;
 
-    // This is an input instead of an Output because the handler is
-    // passed off to the grid context for maintenance -- events
-    // are not fired directly from this component.
     @Output() onChange: EventEmitter<boolean>;
 
+    private cb: GridToolbarCheckbox;
+
     // get a reference to our container grid.
     constructor(@Host() private grid: GridComponent) {
         this.onChange = new EventEmitter<boolean>();
+
+        // Create in constructor so we can accept values before the
+        // grid is fully rendered.
+        this.cb = new GridToolbarCheckbox();
+        this.cb.isChecked = null;
+        this.initialValue = null;
     }
 
     ngOnInit() {
-
         if (!this.grid) {
             console.warn('GridToolbarCheckboxComponent needs a [grid]');
             return;
         }
 
-        const cb = new GridToolbarCheckbox();
-        cb.label = this.label;
-        cb.onChange = this.onChange;
-        cb.isChecked = this.initialValue;
+        this.cb.label = this.label;
+        this.cb.onChange = this.onChange;
+
+        if (this.cb.isChecked === null && this.initialValue !== null) {
+            this.cb.isChecked = this.initialValue;
+        }
+
+        this.grid.context.toolbarCheckboxes.push(this.cb);
+    }
 
-        this.grid.context.toolbarCheckboxes.push(cb);
+    // Toggle the value.  onChange is not fired.
+    toggle() {
+        this.cb.isChecked = !this.cb.isChecked;
+    }
+
+    // Set/get the value.  onChange is not fired.
+    checked(value?: boolean): boolean {
+        if (value === true || value === false) {
+            this.cb.isChecked = value;
+        }
+        return this.cb.isChecked;
     }
 }
 
index 0259cdf..d717c42 100644 (file)
@@ -672,7 +672,7 @@ export class GridContext {
         for (let i = 0; i < steps.length; i++) {
             const step = steps[i];
 
-            if (typeof obj !== 'object') {
+            if (typeof obj !== 'object' || obj === null) {
                 // We have run out of data to step through before
                 // reaching the end of the path.  Conclude fleshing via
                 // callback if provided then exit.
index 4ae2f66..db65e99 100644 (file)
@@ -6,10 +6,10 @@
       <a class="label-with-material-icon" (click)="userContext.toggleExpandRow(row)">
         <!--  leave the icons in place for all node types, but make them
               invisible when they are not needed. -->
-        <span *ngIf="row.expanded"
+        <span *ngIf="row.treeNode.expanded"
           [ngClass]="{invisible: row.copy || row.treeNode.children.length == 0}"
-          lass="material-icons p-0 m-0">arrow_drop_down</span>
-        <span *ngIf="!row.expanded"
+          class="material-icons p-0 m-0">arrow_drop_down</span>
+        <span *ngIf="!row.treeNode.expanded"
           [ngClass]="{invisible: row.copy || row.treeNode.children.length == 0}"
           class="material-icons p-0 m-0">arrow_right</span>
         <span>{{row.locationLabel}}</span>
 
     <!-- checkboxes -->
 
-    <!-- TODO TODO XXXX The onChange handlers will migrate to (onChange) event
-          handler in another pending branch !-->
-    <eg-grid-toolbar-checkbox i18n-label label="Show Volumes"            
-      [onChange]="toggleShowVolumes"></eg-grid-toolbar-checkbox> 
+    <eg-grid-toolbar-checkbox i18n-label label="Show Volumes" 
+      #volsCheckbox (onChange)="toggleShowVolumes($event)">
+    </eg-grid-toolbar-checkbox> 
+    <eg-grid-toolbar-checkbox i18n-label label="Show Copies" 
+      #copiesCheckbox (onChange)="toggleShowCopies($event)">
+    </eg-grid-toolbar-checkbox> 
+    <eg-grid-toolbar-checkbox i18n-label label="Show Empty Volumes"            
+      #emptyVolsCheckbox (onChange)="toggleShowEmptyVolumes($event)">
+    </eg-grid-toolbar-checkbox> 
+    <eg-grid-toolbar-checkbox i18n-label label="Show Empty Libs"            
+      #emptyLibsCheckbox (onChange)="toggleShowEmptyLibs($event)">
+    </eg-grid-toolbar-checkbox> 
 
     <!-- fields -->
     <eg-grid-column path="index" [hidden]="true" [index]="true">
     </eg-grid-column>
     <eg-grid-column path="callNumberLabel" label="Call Number" i18n-label>
     </eg-grid-column>
-
-  <!--
-    <eg-grid-column i18n-label label="Copy ID" path="id" 
-      [hidden]="true" [index]="true">
-    </eg-grid-column>
-    <eg-grid-column i18n-label label="Location" path="circ_lib" datatype="org_unit">
-    </eg-grid-column>
-    <eg-grid-column i18n-label label="Call Number / Copy Notes" 
-      name="callnumber" [cellTemplate]="cnTemplate">
-    </eg-grid-column>
-    <eg-grid-column i18n-label label="Barcode" name="barcode"
-      [cellTemplate]="barcodeTemplate">
-    </eg-grid-column>
-    <eg-grid-column i18n-label label="Shelving Location" path="copy_location">
+    <eg-grid-column i18n-label label="Circ Library" path="copy.circ_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>
-    <eg-grid-column i18n-label label="Circulation Modifier" path="circ_modifier">
+    <eg-grid-column i18n-label label="Circulation Modifier" path="copy.circ_modifier">
     </eg-grid-column>
-    <eg-grid-column i18n-label label="Age Hold Protection" path="age_protect">
+
+    <eg-grid-column i18n-label label="Status" path="copy.status.name">
     </eg-grid-column>
     <eg-grid-column i18n-label label="Active/Create Date" 
-      path="active_date" datatype="timestamp">
+      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>
+
+  <!--
     <eg-grid-column i18n-label label="Holdable?" name="holdable" 
       [cellTemplate]="holdableTemplate" [cellContext]="copyContext">
     </eg-grid-column>
-    <eg-grid-column i18n-label label="Status" path="copy_status">
-    </eg-grid-column>
-    <eg-grid-column i18n-label label="Due Date" path="due_date" datatype="timestamp">
-    </eg-grid-column>
   -->
+
   </eg-grid>
 </div>
 
index 804ad04..ebead26 100644 (file)
@@ -1,5 +1,6 @@
 import {Component, OnInit, Input, ViewChild} from '@angular/core';
 import {Observable, Observer, of} from 'rxjs';
+import {map} from 'rxjs/operators';
 import {Pager} from '@eg/share/util/pager';
 import {IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
@@ -9,6 +10,7 @@ import {AuthService} from '@eg/core/auth.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {GridDataSource} from '@eg/share/grid/grid';
 import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridToolbarCheckboxComponent} from '@eg/share/grid/grid-toolbar-checkbox.component';
 import {ServerStoreService} from '@eg/core/server-store.service';
 
 
@@ -45,6 +47,7 @@ class HoldingsEntry {
     callNumberLabel: string;
     copy: IdlObject;
     volume: IdlObject;
+    circ: IdlObject;
     treeNode: HoldingsTreeNode;
 }
 
@@ -60,20 +63,26 @@ export class HoldingsMaintenanceComponent implements OnInit {
     gridTemplateContext: any;
     @ViewChild('holdingsGrid') holdingsGrid: GridComponent;
 
-    showVolumes: boolean;
-    showCopies: boolean;
-    showEmptyVolumes: boolean;
-    showEmptyLibs: boolean;
+    // Manage visibility of various sub-sections
+    @ViewChild('volsCheckbox') volsCheckbox: GridToolbarCheckboxComponent;
+    @ViewChild('copiesCheckbox') copiesCheckbox: GridToolbarCheckboxComponent;
+    @ViewChild('emptyVolsCheckbox') emptyVolsCheckbox: GridToolbarCheckboxComponent;
+    @ViewChild('emptyLibsCheckbox') emptyLibsCheckbox: GridToolbarCheckboxComponent;
+
     contextOrg: IdlObject;
     holdingsTree: HoldingsTree;
     holdingsTreeOrgCache: {[id: number]: HoldingsTreeNode};
     refreshHoldings: boolean;
     gridIndex: number;
-    rowClassCallback: (row: any) => string;
 
-    // TODO XXXX The checkbox toggles will change to @Output handlers
-    // in a pending catalog working branch.
-    toggleShowVolumes: (value: boolean) => void;
+    // List of copies whose due date we need to retrieve.
+    itemCircsNeeded: IdlObject[];
+
+    // When true draw the grid based on the stored preferences.
+    // 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;
 
     @Input() set recordId(id: number) {
         this.recId = id;
@@ -94,11 +103,10 @@ export class HoldingsMaintenanceComponent implements OnInit {
         private store: ServerStoreService
     ) {
         // Set some sane defaults before settings are loaded.
-        this.showVolumes = true;
-        this.showCopies = true;
         this.contextOrg = this.org.get(this.auth.user().ws_ou());
         this.gridDataSource = new GridDataSource();
         this.refreshHoldings = true;
+        this.renderFromPrefs = true;
 
         this.rowClassCallback = (row: any): string => {
              if (row.volume && !row.copy) {
@@ -123,47 +131,77 @@ export class HoldingsMaintenanceComponent implements OnInit {
                 this.holdingsGrid.reload();
             }
         }
-
-
-        // TODO XXXX The checkbox toggles will change to @Output handlers
-        // in a pending catalog working branch.
-        this.toggleShowVolumes = (value: boolean) => {
-            this.showVolumes = value;
-            this.store.setItem('cat.holdings_show_vols', value);
-            this.holdingsGrid.reload();
-        }
-    }
-
-    onRowActivate(row: any) {
-        if (row.copy) {
-            // Launch copy editor?
-        } else {
-            this.gridTemplateContext.toggleExpandRow(row);
-        }
     }
 
     ngOnInit() {
         this.initDone = true;
 
         // These are pre-cached via the resolver.
-        this.store.getItemBatch([
+        const settings = this.store.getItemBatchCached([
             'cat.holdings_show_empty_org',
             'cat.holdings_show_empty',
             'cat.holdings_show_copies',
             'cat.holdings_show_vols'
-        ]).then(settings => {
-            this.showCopies = settings['cat.holdings_show_copies'];
-            console.log('show copies = ', this.showCopies);
-            this.showVolumes = settings['cat.holdings_show_vols'];
-            this.showEmptyVolumes = settings['cat.holdings_show_empty'];
-            this.showEmptyLibs = settings['cat.holdings_show_empty_org'];
-        });
+        ]);
+
+        this.volsCheckbox.checked(settings['cat.holdings_show_vols']);
+        this.copiesCheckbox.checked(settings['cat.holdings_show_copies']);
+        this.emptyVolsCheckbox.checked(settings['cat.holdings_show_empty']);
+        this.emptyLibsCheckbox.checked(settings['cat.holdings_show_empty_org']);
 
         this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
             return this.fetchHoldings(pager);
         };
     }
 
+    ngAfterViewInit() {
+
+    }
+
+    toggleShowCopies(value: boolean) {
+        this.store.setItem('cat.holdings_show_copies', value);
+        if (value) {
+            // Showing copies implies showing volumes
+            this.volsCheckbox.checked(true);
+        }
+        this.renderFromPrefs = true;
+        this.holdingsGrid.reload();
+    }
+
+    toggleShowVolumes(value: boolean) {
+        this.store.setItem('cat.holdings_show_vols', value);
+        if (!value) {
+            // Hiding volumes implies hiding empty vols and copies.
+            this.copiesCheckbox.checked(false);
+            this.emptyVolsCheckbox.checked(false);
+        }
+        this.renderFromPrefs = true;
+        this.holdingsGrid.reload();
+    }
+
+    toggleShowEmptyVolumes(value: boolean) {
+        this.store.setItem('cat.holdings_show_empty', value);
+        if (value) {
+            this.volsCheckbox.checked(true);
+        }
+        this.renderFromPrefs = true;
+        this.holdingsGrid.reload();
+    }
+
+    toggleShowEmptyLibs(value: boolean) {
+        this.store.setItem('cat.holdings_show_empty_org', value);
+        this.renderFromPrefs = true;
+        this.holdingsGrid.reload();
+    }
+
+    onRowActivate(row: any) {
+        if (row.copy) {
+            // Launch copy editor?
+        } else {
+            this.gridTemplateContext.toggleExpandRow(row);
+        }
+    }
+
     initHoldingsTree() {
 
         // The initial tree simply matches the org unit tree
@@ -210,6 +248,7 @@ export class HoldingsMaintenanceComponent implements OnInit {
     }
 
     // Sets call number and copy count sums to nodes that need it.
+    // Applies the initial expansed state of each container node.
     setTreeCounts(node: HoldingsTreeNode) {
 
         if (node.nodeType === 'org') {
@@ -219,6 +258,8 @@ export class HoldingsMaintenanceComponent implements OnInit {
             node.copyCount = 0;
         }
 
+        let hasChildOrgWithData = false;
+        let hasChildOrgSansData = false;
         node.children.forEach(child => {
             this.setTreeCounts(child);
             if (node.nodeType === 'org') {
@@ -226,12 +267,31 @@ export class HoldingsMaintenanceComponent implements OnInit {
                 if (child.nodeType === 'volume') {
                     node.volumeCount++;
                 } else {
+                    hasChildOrgWithData = child.volumeCount > 0;
+                    hasChildOrgSansData = child.volumeCount === 0;
                     node.volumeCount += child.volumeCount;
                 }
             } else if (node.nodeType === 'volume') {
                 node.copyCount = node.children.length;
+                if (this.renderFromPrefs) {
+                    node.expanded = this.copiesCheckbox.checked();
+                }
             }
         });
+
+        if (this.renderFromPrefs && node.nodeType === 'org') {
+            if (node.copyCount > 0 && this.volsCheckbox.checked()) {
+                node.expanded = true;
+            } else if (node.volumeCount > 0 && this.emptyVolsCheckbox.checked()) {
+                node.expanded = true;
+            } else if (hasChildOrgWithData) {
+                node.expanded = true;
+            } else if (hasChildOrgSansData && this.emptyLibsCheckbox.checked()) {
+                node.expanded = true;
+            } else {
+                node.expanded = false;
+            }
+        }
     }
 
     // Create HoldingsEntry objects for tree nodes that should be displayed
@@ -243,11 +303,9 @@ export class HoldingsMaintenanceComponent implements OnInit {
 
         switch(node.nodeType) {
             case 'org':
-                // Confirm the user wants to see this node
-                if (!this.showEmptyLibs) {
-                    if (node.volumeCount === 0) {
-                        return;
-                    }
+                if (this.renderFromPrefs && node.volumeCount === 0
+                    && !this.emptyLibsCheckbox.checked()) {
+                    return;
                 }
                 entry.locationLabel = node.target.shortname();
                 entry.locationDepth = node.target.ou_type().depth();
@@ -257,14 +315,6 @@ export class HoldingsMaintenanceComponent implements OnInit {
                 break;
 
             case 'volume':
-                // Confirm the user wants to see this node
-                if (!this.showVolumes) {
-                    return;
-                }
-                if (!this.showEmptyVolumes && node.copyCount === 0) {
-                    return;
-                }
-
                 entry.locationLabel = node.target.label(); // TODO prefix/suffix
                 entry.locationDepth = node.parentNode.target.ou_type().depth() + 1;
                 entry.callNumberLabel = entry.locationLabel;
@@ -278,6 +328,7 @@ export class HoldingsMaintenanceComponent implements OnInit {
                 entry.callNumberLabel = node.parentNode.target.label() // TODO
                 entry.volume = node.parentNode.target;
                 entry.copy = node.target;
+                entry.circ = node.target._circ;
                 break;
         }
 
@@ -297,6 +348,7 @@ export class HoldingsMaintenanceComponent implements OnInit {
         this.setTreeCounts(this.holdingsTree.root);
         this.propagateTreeEntries(observer, this.holdingsTree.root);
         observer.complete();
+        this.renderFromPrefs = false;
     }
 
     fetchHoldings(pager: Pager): Observable<any> {
@@ -310,6 +362,7 @@ export class HoldingsMaintenanceComponent implements OnInit {
             }
 
             this.initHoldingsTree();
+            this.itemCircsNeeded = [];
 
             this.pcrud.search('acn',
                 {   record: this.recId,
@@ -330,12 +383,29 @@ export class HoldingsMaintenanceComponent implements OnInit {
                 err => {},
                 ()  => {
                     this.refreshHoldings = false;
-                    this.flattenHoldingsTree(observer);
+                    this.fetchCircs().then(
+                        ok => this.flattenHoldingsTree(observer)
+                    );
                 }
             );
         });
     }
 
+    // Retrieve circulation objects for checked out items.
+    fetchCircs(): Promise<any> {
+        const copyIds = this.itemCircsNeeded.map(copy => copy.id());
+        if (copyIds.length === 0) { return Promise.resolve(); }
+
+        return this.pcrud.search('circ', {
+            target_copy: copyIds,
+            checkin_time: null
+        }).pipe(map(circ => {
+            const copy = this.itemCircsNeeded.filter(
+                c => Number(c.id()) === Number(circ.target_copy()))[0];
+            copy._circ = circ;
+        })).toPromise();
+    }
+
     appendVolume(volume: IdlObject) {
 
         const volNode = new HoldingsTreeNode();
@@ -343,7 +413,6 @@ export class HoldingsMaintenanceComponent implements OnInit {
         volNode.parentNode.children.push(volNode);
         volNode.nodeType = 'volume';
         volNode.target = volume;
-        volNode.expanded = true; // TODO if show volumes
 
         volume.copies()
             .sort((a: IdlObject, b: IdlObject) => a.barcode() < b.barcode() ? -1 : 1)
@@ -353,9 +422,11 @@ export class HoldingsMaintenanceComponent implements OnInit {
                 volNode.children.push(copyNode);
                 copyNode.nodeType = 'copy';
                 copyNode.target = copy;
-                copyNode.expanded = true; // TODO if show copies
+                const stat = Number(copy.status().id());
+                if (stat === 1 /* checked out */ || stat === 16 /* long overdue */) {
+                    this.itemCircsNeeded.push(copy);
+                }
             });
-
     }
 }