LPXXX Angular Volcopy
authorBill Erickson <berickxx@gmail.com>
Fri, 5 Jun 2020 20:54:23 +0000 (16:54 -0400)
committerBill Erickson <berickxx@gmail.com>
Thu, 25 Jun 2020 14:36:18 +0000 (10:36 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.css
Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.html
Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.html
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.module.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.ts
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts
Open-ILS/web/js/ui/default/staff/cat/catalog/app.js

index c109cac..fabadae 100644 (file)
@@ -9,3 +9,18 @@ input[type="number"] {
   border-top: 1px solid rgba(0,0,0,.125);
   border-bottom: 1px solid rgba(0,0,0,.125);
 }
+
+
+.clear-button {
+  border: none;
+  background-color: rgba(0, 0, 0, 0.0);
+  padding-left: .25rem;
+  padding-right: .25rem;
+  line-height: inherit;
+}
+
+.clear-button .material-icons {
+  font-size: 15px;
+  color: grey;
+}
+
index 42fd354..673c9c7 100644 (file)
@@ -1,31 +1,42 @@
+<eg-confirm-dialog 
+  #confirmDelVol
+  i18n-dialogTitle i18n-dialogBody
+  dialogTitle="Delete Call Number?"
+  dialogBody="Delete {{deleteVolCount}} Call Number(s) and {{deleteCopyCount}} Associated Item(s)?">
+</eg-confirm-dialog>
+
+<eg-confirm-dialog 
+  #confirmDelCopy
+  i18n-dialogTitle i18n-dialogBody
+  dialogTitle="Delete Item?"
+  dialogBody="Delete {{deleteCopyCount}} Item(s)?">
+</eg-confirm-dialog>
 
 <div class="row d-flex vol-row border border-info mb-2">
-  <div class="p-1" [ngStyle]="{flex: flexAt(1)}">
-  </div>
-  <div class="p-1" [ngStyle]="{flex: flexAt(2)}">
-  </div>
+  <div class="p-1" [ngStyle]="{flex: flexAt(1)}"> </div>
+  <div class="p-1" [ngStyle]="{flex: flexAt(2)}"> </div>
   <div class="p-1" [ngStyle]="{flex: flexAt(3)}">
-    <label class="font-weight-bold" i18n>Classification</label>
+    <div><label class="font-weight-bold" i18n>Classification</label></div>
     <div>
       <eg-combobox [smallFormControl]="true" [(ngModel)]="batchVolClass">
-        <eg-combobox-entry *ngFor="let cls of volClasses" 
+        <eg-combobox-entry *ngFor="let cls of volClasses"
           [entryId]="cls.id()" [entryLabel]="cls.name()">
         </eg-combobox-entry>
       </eg-combobox>
     </div>
   </div>
   <div class="p-1" [ngStyle]="{flex: flexAt(4)}">
-    <label class="font-weight-bold" i18n>Prefix</label>
+    <div><label class="font-weight-bold" i18n>Prefix</label></div>
     <div>
       <eg-combobox [smallFormControl]="true" [(ngModel)]="batchVolPrefix">
-        <eg-combobox-entry *ngFor="let pfx of volPrefixes" 
+        <eg-combobox-entry *ngFor="let pfx of volPrefixes"
           [entryId]="pfx.id()" [entryLabel]="pfx.label()">
         </eg-combobox-entry>
       </eg-combobox>
     </div>
   </div>
   <div class="p-1" [ngStyle]="{flex: flexAt(5)}">
-    <label class="font-weight-bold" i18n>Call Number Label</label>
+    <div><label class="font-weight-bold" i18n>Call Number Label</label></div>
     <div>
       <eg-combobox [smallFormControl]="true" [(ngModel)]="batchVolLabel">
         <eg-combobox-entry *ngFor="let label of recordVolLabels" [entryId]="label">
     </div>
   </div>
   <div class="p-1" [ngStyle]="{flex: flexAt(6)}">
-    <label class="font-weight-bold" i18n>Suffix</label>
+    <div><label class="font-weight-bold" i18n>Suffix</label></div>
     <div>
       <eg-combobox [smallFormControl]="true" [(ngModel)]="batchVolSuffix">
-        <eg-combobox-entry *ngFor="let sfx of volSuffixes" 
+        <eg-combobox-entry *ngFor="let sfx of volSuffixes"
           [entryId]="sfx.id()" [entryLabel]="sfx.label()">
         </eg-combobox-entry>
       </eg-combobox>
     </div>
   </div>
   <div class="p-1" [ngStyle]="{flex: flexAt(7)}">
-    <label class="font-weight-bold" i18n>Batch</label>
+    <div><label class="font-weight-bold" i18n>Batch</label></div>
     <div>
-      <button class="btn btn-sm btn-outline-dark label-with-material-icon" 
+      <button class="btn btn-sm btn-outline-dark label-with-material-icon"
         (click)="batchVolApply()">
         <span i18n>Apply</span>
         <span class="material-icons">arrow_downward</span>
     </div>
   </div>
   <div class="p-1" [ngStyle]="{flex: flexAt(8)}">
-    <label class="font-weight-bold" i18n>Generate Barcodes</label>
-    <button class="btn btn-sm btn-outline-dark label-with-material-icon" 
+    <div><label class="font-weight-bold" i18n>Generate Barcodes</label></div>
+    <button class="btn btn-sm btn-outline-dark label-with-material-icon"
       (click)="generateBarcodes()">
       <span i18n>Generate</span>
       <span class="material-icons">arrow_downward</span>
     </button>
   </div>
-  <div class="p-1" [ngStyle]="{flex: flexAt(9)}"></div>
-  <div class="p-1" [ngStyle]="{flex: flexAt(10)}"></div>
+  <div class="p-1" [ngStyle]="{flex: flexSpan(9, 10)}">
+    <div><label class="font-weight-bold" i18n>Checkdigit</label></div>
+    <div class="form-check form-check-inline">
+      <input class="form-check-input" type="checkbox" 
+        id="use-checkdigit" [(ngModel)]="useCheckdigit">
+      <label class="form-check-label" for="use-checkdigit" i18n>
+        Use Checkdigit
+      </label>
+    </div>
+  </div>
 </div>
 
 
     <ng-container *ngFor="let copyNode of volNode.children; let copyIdx = index">
       <div class="row d-flex mt-1" [ngClass]="{'vol-row': copyIdx == 0}">
         <div class="p-1" [ngStyle]="{flex: flexAt(1)}">
-          <span *ngIf="copyIdx == 0">{{orgNode.target.shortname()}}</span>
+          <ng-container *ngIf="copyIdx == 0">
+            <span>{{orgNode.target.shortname()}}</span>
+            {{sessionType}}
+            <ng-container *ngIf="context.sessionType == 'record' || context.sessionType == 'mixed'">
+              <button class="clear-button" (click)="deleteVol(volNode)"
+                title="Delete Call Number {{volNode.target.label()}}" i18n-title>
+                <span class="material-icons">clear</span>
+              </button>
+            </ng-container>
+          </ng-container>
         </div>
         <div class="p-1" [ngStyle]="{flex: flexAt(2)}">
-          <ng-container *ngIf="copyIdx == 0">
+          <ng-container *ngIf="copyIdx == 0 && volIdx == 0">
             <input type="number" class="form-control form-control-sm"
-              [required]="true"
+              [disabled]="context.sessionType == 'copy' || context.sessionType == 'vol'"
+              [required]="true" [min]="existingVolCount(orgNode)"
               [ngModel]="orgNode.children.length"
               (ngModelChange)="volCountChanged(orgNode, $event)"/>
           </ng-container>
         </div>
         <div class="p-1" [ngStyle]="{flex: flexAt(3)}">
           <ng-container *ngIf="copyIdx == 0">
-            <eg-combobox 
+            <eg-combobox
               [selectedId]="volNode.target.label_class()"
               [smallFormControl]="true"
-              [required]="true" 
+              [required]="true"
               (onChange)="applyVolValue(volNode.target, 'label_class', $event ? $event.id : null)">
-              <eg-combobox-entry *ngFor="let cls of volClasses" 
+              <eg-combobox-entry *ngFor="let cls of volClasses"
                 [entryId]="cls.id()" [entryLabel]="cls.name()">
               </eg-combobox-entry>
             </eg-combobox>
         </div>
         <div class="p-1" [ngStyle]="{flex: flexAt(4)}">
           <ng-container *ngIf="copyIdx == 0">
-            <eg-combobox 
+            <eg-combobox
               [selectedId]="volNode.target.prefix()"
-              [required]="true" 
+              [required]="true"
               [smallFormControl]="true"
               (onChange)="applyVolValue(volNode.target, 'prefix', $event ? $event.id : null)">
-              <eg-combobox-entry *ngFor="let pfx of volPrefixes" 
+              <eg-combobox-entry *ngFor="let pfx of volPrefixes"
                 [entryId]="pfx.id()" [entryLabel]="pfx.label()">
               </eg-combobox-entry>
             </eg-combobox>
               spellcheck="false"
               [required]="true"
               [ngModel]="volNode.target.label()"
-              (onChange)="applyVolValue(volNode.target, 'label', $event)">
+              (change)="applyVolValue(volNode.target, 'label', $event.target.value)">
           </ng-container>
         </div>
         <div class="p-1" [ngStyle]="{flex: flexAt(6)}">
           <ng-container *ngIf="copyIdx == 0">
-            <eg-combobox 
+            <eg-combobox
               [selectedId]="volNode.target.suffix()"
-              [required]="true" 
+              [required]="true"
               [smallFormControl]="true"
               (onChange)="applyVolValue(volNode.target, 'suffix', $event ? $event.id : null)">
-              <eg-combobox-entry *ngFor="let sfx of volSuffixes" 
+              <eg-combobox-entry *ngFor="let sfx of volSuffixes"
                 [entryId]="sfx.id()" [entryLabel]="sfx.label()">
               </eg-combobox-entry>
             </eg-combobox>
         <div class="p-1" [ngStyle]="{flex: flexAt(7)}">
           <ng-container *ngIf="copyIdx == 0">
             <input type="number" class="form-control form-control-sm"
-              [required]="true"
+              [disabled]="context.sessionType == 'copy'"
+              [required]="true" [min]="existingCopyCount(volNode)"
               [ngModel]="volNode.children.length"
               (ngModelChange)="copyCountChanged(volNode, $event)"/>
           </ng-container>
         </div>
         <div class="p-1" [ngStyle]="{flex: flexAt(8)}">
-          <input type="text" class="form-control form-control-sm"
-            id="barcode-input-{{copyNode.target.id()}}"
-            spellcheck="false"
-            [required]="true"
-            (keyup.enter)="selectNextBarcode(copyNode.target.id())"
-            (keyup.shift.enter)="selectNextBarcode(copyNode.target.id(), true)"
-            [ngModel]="copyNode.target.barcode()"
-            (ngModelChange)="applyCopyValue(copyNode.target, 'barcode', $event)"/>
+
+          <div class="d-flex">
+            <ng-container *ngIf="context.sessionType != 'copy'">
+              <button class="clear-button" (click)="deleteCopy(copyNode)"
+                title="Delete Item {{copyNode.target.barcode()}}" i18n-title>
+                <span class="material-icons">clear</span>
+              </button>
+            </ng-container>
+
+            <input type="text" class="form-control form-control-sm"
+              id="barcode-input-{{copyNode.target.id()}}"
+              spellcheck="false" [required]="true"
+              [ngClass]="{'text-danger': copyNode.target._dupe_barcode}"
+              (change)="barcodeChanged(copyNode.target, $event.target.value)"  
+              (ngModelChange)="copyNode.target.barcode($event)"
+              (keyup.enter)="selectNextBarcode(copyNode.target.id())"
+              (keyup.shift.enter)="selectNextBarcode(copyNode.target.id(), true)"
+              (focus)="$event.target.select()"
+              [ngModel]="copyNode.target.barcode()"
+              (ngModelChange)="applyCopyValue(copyNode.target, 'barcode', $event)"/>
+          </div>
+          <div *ngIf="copyNode.target._dupe_barcode"
+            class="alert alert-danger font-italic p-1" i18n>
+            Duplicate Barcode
+          </div>
         </div>
         <div class="p-1" [ngStyle]="{flex: flexAt(9)}">
-          <input type="number" class="form-control form-control-sm"
+          <input type="number" min="1" class="form-control form-control-sm"
             [ngModel]="copyNode.target.copy_number()"
             (ngModelChange)="applyCopyValue(copyNode.target, 'copy_number', $event)"/>
         </div>
             <label i18n>N/A</label>
           </ng-container>
           <ng-container *ngIf="recordHasParts(volNode.target.record())">
-            <eg-combobox 
+            <eg-combobox
               [disabled]="bibParts[volNode.target.record()].length == 0"
               [selectedId]="copyNode.target.parts()[0] ? copyNode.target.parts()[0].id() : null"
               [smallFormControl]="true"
index 6b9c33a..20abe09 100644 (file)
@@ -1,12 +1,16 @@
 import {Component, OnInit, AfterViewInit, ViewChild, Input, Renderer2} from '@angular/core';
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
-import {IdlObject} from '@eg/core/idl.service';
+import {tap} from 'rxjs/operators';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
 import {NetService} from '@eg/core/net.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {VolCopyContext, HoldingsTreeNode} from './volcopy';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {VolCopyService} from './volcopy.service';
 
 @Component({
   selector: 'eg-vol-edit',
@@ -36,19 +40,41 @@ export class VolEditComponent implements OnInit {
     batchVolSuffix: ComboboxEntry;
     batchVolLabel: ComboboxEntry;
 
+    autoBarcodeInProgress = false;
+    useCheckdigit = false;
+
+    autoId = -1;
+
+    deleteVolCount: number = null;
+    deleteCopyCount: number = null;
+
     recordVolLabels: string[] = [];
 
+    @ViewChild('confirmDelVol', {static: false})
+        confirmDelVol: ConfirmDialogComponent;
+
+    @ViewChild('confirmDelCopy', {static: false})
+        confirmDelCopy: ConfirmDialogComponent;
+
     constructor(
         private renderer: Renderer2,
+        private idl: IdlService,
         private pcrud: PcrudService,
         private net: NetService,
-        private holdings: HoldingsService
+        private auth: AuthService,
+        private holdings: HoldingsService,
+        private volcopy: VolCopyService
     ) {}
 
     ngOnInit() {
 
-        this.fetchRecordVolLabels()
-        .then(_ => this.fetchBibParts());
+        this.deleteVolCount = null;
+        this.deleteCopyCount = null;
+
+        this.volcopy.fetchRecordVolLabels(this.context.recordId)
+        .then(labels => this.recordVolLabels = labels)
+        .then(_ => this.fetchBibParts())
+        .then(_ => this.addStubCopies());
 
         // TODO: Filter these to only show org-scoped values
         // plus any values otherwise needed for the current
@@ -65,25 +91,6 @@ export class VolEditComponent implements OnInit {
             this.volSuffixes = suffixes.filter(pfx => pfx.id() !== -1));
     }
 
-    fetchRecordVolLabels(): Promise<any> {
-        // NOTE: see https://bugs.launchpad.net/evergreen/+bug/1874897
-        // for more on MARC call numbers and classification scheme.
-
-        this.recordVolLabels = [];
-        const ids = this.context.getRecordIds();
-
-        // It only makes sense to fetch bib call numbers when we are
-        // working with exactly one record.
-        if (ids.length !== 1) { return Promise.resolve(); }
-
-        return this.net.request(
-            'open-ils.cat',
-            'open-ils.cat.biblio.record.marc_cn.retrieve', ids[0]
-        ).toPromise().then(res => {
-            this.recordVolLabels = Object.values(res)
-                .map(blob => Object.values(blob)[0]).sort();
-        });
-    }
 
     fetchBibParts() {
 
@@ -121,12 +128,107 @@ export class VolEditComponent implements OnInit {
         return this.flexSettings[column];
     }
 
+    // Returns the flex amount occupied by a span of columns.
+    flexSpan(column1: number, column2: number): number {
+        let flex = 0;
+        for (let i = column1; i <= column2; i++) {
+            flex += this.flexSettings[i];
+        }
+        return flex;
+    }
+
     volCountChanged(orgNode: HoldingsTreeNode, count: number) {
-        console.log('vol set set to ', count);
+        if (count === null) { return; }
+        const diff = count - orgNode.children.length;
+        if (diff > 0) {
+            this.createVols(orgNode, diff);
+        } else if (diff < 0) {
+            this.deleteVols(orgNode, -diff);
+        }
+    }
+
+    existingVolCount(orgNode: HoldingsTreeNode): number {
+        return orgNode.children.filter(volNode => !volNode.target.isnew()).length;
+    }
+
+    existingCopyCount(volNode: HoldingsTreeNode): number {
+        return volNode.children.filter(copyNode => !copyNode.target.isnew()).length;
     }
 
     copyCountChanged(volNode: HoldingsTreeNode, count: number) {
-        console.log('vol set set to ', count);
+        if (count === null) { return; }
+        const diff = count - volNode.children.length;
+        if (diff > 0) {
+            this.createCopies(volNode, diff);
+        } else if (diff < 0) {
+            this.deleteCopies(volNode, -diff);
+        }
+    }
+
+    // This only removes copies that were created during the
+    // current editing session and have not yet been saved in the DB.
+    deleteCopies(volNode: HoldingsTreeNode, count: number) {
+        for (let i = 0;  i < count; i++) {
+            const copyNode = volNode.children[volNode.children.length - 1];
+            if (copyNode && copyNode.target.isnew()) {
+                volNode.children.pop();
+            } else {
+                break;
+            }
+        }
+    }
+
+    createCopies(volNode: HoldingsTreeNode, count: number) {
+        for (let i = 0; i < count; i++) {
+
+            // Our context assumes copies are fleshed with volumes
+            const vol = volNode.target;
+            const copy = this.volcopy.createStubCopy(vol);
+            copy.call_number(vol);
+            this.context.findOrCreateCopyNode(copy);
+        }
+    }
+
+
+    createVols(orgNode: HoldingsTreeNode, count: number) {
+        for (let i = 0; i < count; i++) {
+
+            // This will vivify the volNode if needed.
+            const vol = this.volcopy.createStubVol(
+                this.context.recordId, orgNode.target.id())
+
+            // Our context assumes copies are fleshed with volumes
+            const copy = this.volcopy.createStubCopy(vol);
+            copy.call_number(vol);
+            this.context.findOrCreateCopyNode(copy);
+        }
+    }
+
+    // This only removes vols that were created during the
+    // current editing session and have not yet been saved in the DB.
+    deleteVols(orgNode: HoldingsTreeNode, count: number) {
+        for (let i = 0;  i < count; i++) {
+            const volNode = orgNode.children[orgNode.children.length - 1];
+            if (volNode && volNode.target.isnew()) {
+                orgNode.children.pop();
+            } else {
+                break;
+            }
+        }
+    }
+
+    // Empty volumes get a stub copy
+    addStubCopies(volNode?: HoldingsTreeNode) {
+        const nodes = volNode ? [volNode] : this.context.volNodes();
+
+        nodes.forEach(volNode => {
+            if (volNode.children.length == 0) {
+                const vol = volNode.target;
+                const copy = this.volcopy.createStubCopy(vol);
+                copy.call_number(vol);
+                this.context.findOrCreateCopyNode(copy);
+            }
+        });
     }
 
     applyVolValue(vol: IdlObject, key: string, value: any) {
@@ -156,7 +258,6 @@ export class VolEditComponent implements OnInit {
     batchVolApply() {
         this.context.volNodes().forEach(volNode => {
             const vol = volNode.target;
-            console.log('batch vol class', this.batchVolClass.id);
             if (this.batchVolClass) {
                 this.applyVolValue(vol, 'label_class', this.batchVolClass.id);
             }
@@ -201,7 +302,184 @@ export class VolEditComponent implements OnInit {
                 '#barcode-input-' + (nextId || firstId)).select();
     }
 
+    barcodeCanChange(copy: IdlObject): boolean {
+        // TODO
+        return true;
+    }
+
     generateBarcodes() {
+               this.autoBarcodeInProgress = true;
+
+        // Autogen only replaces barcodes for items which are in
+        // certain statuses.
+        const copies = this.context.copyList()
+        .filter((copy, idx) => {
+            // During autogen we do not replace the first item,
+            // so it's status is not relevant.
+            return idx === 0 || this.barcodeCanChange(copy);
+        });
+
+        if (copies.length > 1) { // seed barcode will always be present
+            this.proceedWithAutogen(copies)
+            .then(_ => this.autoBarcodeInProgress = false);
+        };
+    }
+
+    proceedWithAutogen(copyList: IdlObject[]): Promise<any> {
+
+        const seedBarcode: string = copyList[0].barcode();
+        copyList.shift(); // Avoid replacing the seed barcode
+
+        const count = copyList.length;
+
+        return this.net.request('open-ils.cat',
+            'open-ils.cat.item.barcode.autogen',
+            this.auth.token(), seedBarcode, count, {
+                checkdigit: this.useCheckdigit,
+                skip_dupes: true
+            }
+        ).pipe(tap(barcodes => {
+
+            copyList.forEach(copy => {
+                if (copy.barcode() !== barcodes[0]) {
+                    copy.barcode(barcodes[0]);
+                    copy.ischanged(true);
+                }
+                barcodes.shift();
+            });
+
+        })).toPromise();
+    }
+
+       barcodeChanged(copy: IdlObject, barcode: string) {
+        copy.barcode(barcode);
+        copy.ischanged(true);
+        copy._dupe_barcode = false;
+
+        if (barcode && !this.autoBarcodeInProgress) {
+            // Manual barcode entry requires dupe check
+
+            copy._dupe_barcode = false;
+            this.pcrud.search('acp', {
+                deleted: 'f',
+                barcode: barcode,
+                id: {'!=': copy.id()}
+            }).subscribe(resp => {
+                if (resp) { copy._dupe_barcode = true; }
+            });
+        }
+    }
+
+    deleteCopy(copyNode: HoldingsTreeNode) {
+
+        if (copyNode.target.isnew()) {
+            // Confirmation not required when deleting brand new copies.
+            this.deleteOneCopy(copyNode);
+            return;
+        }
+
+        this.deleteCopyCount = 1;
+        this.confirmDelCopy.open().toPromise().then(confirmed => {
+            if (confirmed) { this.deleteOneCopy(copyNode); }
+        });
+    }
+
+    deleteOneCopy(copyNode: HoldingsTreeNode) {
+
+        const targetCopy = copyNode.target;
+
+        const orgNodes = this.context.orgNodes();
+        for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
+            const orgNode = orgNodes[orgIdx];
+
+            for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
+                const volNode = orgNode.children[volIdx];
+
+                for (let copyIdx = 0; copyIdx < volNode.children.length; copyIdx++) {
+                    const copy = volNode.children[copyIdx].target;
+
+                    if (copy.id() === targetCopy.id()) {
+                        volNode.children.splice(copyIdx, 1);
+                        if (!copy.isnew()) {
+                            copy.isdeleted(true);
+                            this.context.copiesToDelete.push(copy);
+                        }
+
+                        if (volNode.children.length === 0) {
+                            // When removing the last copy, add a stub copy.
+                            this.addStubCopies();
+                        }
+
+                        return;
+                    }
+                }
+            }
+        }
+    }
+
+
+    deleteVol(volNode: HoldingsTreeNode) {
+
+        if (volNode.target.isnew()) {
+            // Confirmation not required when deleting brand new vols.
+            this.deleteOneVol(volNode);
+            return;
+        }
+
+        this.deleteVolCount = 1;
+        this.deleteCopyCount = volNode.children.length;
+
+        this.confirmDelVol.open().toPromise().then(confirmed => {
+            if (confirmed) { this.deleteOneVol(volNode); }
+        });
+    }
+
+    deleteOneVol(volNode: HoldingsTreeNode) {
+        let deleteVolIdx = null;
+        const targetVol = volNode.target;
+
+        // FOR loops allow for early exit
+        const orgNodes = this.context.orgNodes();
+        for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
+            const orgNode = orgNodes[orgIdx];
+
+            for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
+                const vol = orgNode.children[volIdx].target;
+
+                if (vol.id() === targetVol.id()) {
+                    deleteVolIdx = volIdx;
+
+                    if (vol.isnew()) {
+                        // New volumes, which can only have new copies
+                        // may simply be removed from the holdings
+                        // tree to delete them.
+                        break;
+                    }
+
+                    // Mark volume and attached copies as deleted
+                    // and track for later deletion.
+                    targetVol.isdeleted(true);
+                    this.context.volsToDelete.push(targetVol);
+
+                    volNode.children.forEach(copyNode => {
+                        const copy = copyNode.target;
+                        if (copy.isnew()) {
+                            // New copies can simply be discarded.
+                        } else {
+                            copy.isdeleted(true);
+                            this.context.copiesToDelete.push(copy);
+                        }
+                    });
+                }
+
+                if (deleteVolIdx !== null) { break; }
+            }
+
+            if (deleteVolIdx !== null) {
+                orgNode.children.splice(deleteVolIdx, 1);
+                break;
+            }
+        }
     }
 }
 
index df54f57..2f569ea 100644 (file)
@@ -1,11 +1,30 @@
 <eg-staff-banner bannerText="Holdings Editor" i18n-bannerText></eg-staff-banner>
 
+<div class="row" [hidden]="!loading">
+  <div class="col-lg-6 offset-lg-3">
+    <eg-progress-inline #loadingProgress></eg-progress-inline>
+  </div>
+</div>
+
 <ng-container *ngIf="!loading">
 
-  <eg-bib-summary *ngIf="recordId" [recordId]="recordId"></eg-bib-summary>
+  <eg-bib-summary *ngIf="context.recordId" [recordId]="context.recordId"></eg-bib-summary>
   
   <div class="mt-3">
     <eg-vol-edit [context]="context"></eg-vol-edit>
   </div>
 
+
+  <div class="row m-2 p-2 border border-info">
+    <div class="col-lg-12 d-flex">
+      <div class="flex-1"> </div>
+      <button class="btn btn-outline-dark" 
+        [disabled]="!context.isSaveable()" (click)="save()" i18n>Save</button>
+      <button class="btn btn-outline-dark ml-2" 
+        [disabled]="!context.isSaveable()"
+        (click)="save(true)" i18n>Save &amp; Exit</button>
+    </div>
+  </div>
+
 </ng-container>
+
index 8a5999e..cddbfc9 100644 (file)
@@ -1,11 +1,17 @@
 import {Component, OnInit, AfterViewInit, ViewChild, Renderer2} from '@angular/core';
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
 import {tap} from 'rxjs/operators';
-import {IdlObject} from '@eg/core/idl.service';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
 import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
 import {PcrudService} from '@eg/core/pcrud.service';
-import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
+import {HoldingsService, CallNumData} from '@eg/staff/share/holdings/holdings.service';
 import {VolCopyContext} from './volcopy';
+import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
+import {AnonCacheService} from '@eg/share/util/anon-cache.service';
+import {VolCopyService} from './volcopy.service';
 
 const COPY_FLESH = {
     flesh: 1,
@@ -14,32 +20,45 @@ const COPY_FLESH = {
     }
 }
 
+interface EditSession {
+
+    // Unset if editing in multi-record mode
+    record_id: number;
+
+    // Adding to or creating new call numbers
+    raw: CallNumData[];
+
+    // Hide the volumes editor
+    hide_vols: boolean;
+
+    // Hide the copy attrs editor.
+    hide_copies: boolean;
+}
+
 @Component({
   templateUrl: 'volcopy.component.html'
 })
 export class VolCopyComponent implements OnInit {
 
     context: VolCopyContext;
-
-    // Note in multi-record mode this value will be unset.
-    recordId: number;
-
-    // Load specific call number by ID.
-    volId: number;
-
-    // Load specific copy by ID.
-    copyId: number;
-
-    session: string;
     loading = true;
 
+    @ViewChild('loadingProgress', {static: false})
+    loadingProgress: ProgressInlineComponent;
+
     constructor(
         private router: Router,
         private route: ActivatedRoute,
         private renderer: Renderer2,
+        private evt: EventService,
+        private idl: IdlService,
         private org: OrgService,
+        private net: NetService,
+        private auth: AuthService,
         private pcrud: PcrudService,
-        private holdings: HoldingsService
+        private cache: AnonCacheService,
+        private holdings: HoldingsService,
+        private volcopy: VolCopyService
     ) { }
 
     ngOnInit() {
@@ -51,42 +70,137 @@ export class VolCopyComponent implements OnInit {
     }
 
     negotiateRoute(params: ParamMap) {
-        this.recordId = +params.get('record_id') || null;
-        this.volId    = +params.get('vol_id')    || null;
-        this.copyId   = +params.get('copy_id')   || null;
-        this.session  =  params.get('session')   || null;
+        this.context.recordId = +params.get('record_id') || null;
+        this.context.volId    = +params.get('vol_id')    || null;
+        this.context.copyId   = +params.get('copy_id')   || null;
+        this.context.session  =  params.get('session')   || null;
         this.load();
     }
 
     load() {
+
         this.loading = true;
         this.context.reset();
+
         this.fetchHoldings()
+        .then(_ => this.volcopy.applyVolLabels(
+            this.context.volNodes().map(n => n.target)))
         .then(_ => this.holdings.fetchCallNumberClasses())
         .then(_ => this.holdings.fetchCallNumberPrefixes())
         .then(_ => this.holdings.fetchCallNumberSuffixes())
         .then(_ => this.context.sortHoldings())
-        .then(_ => this.setRecordId())
+        .then(_ => this.context.setRecordId())
         .then(_ => this.loading = false);
     }
 
-    setRecordId() {
-        if (!this.recordId) {
-            const ids = this.context.getRecordIds();
-            if (ids.length === 1) {
-                this.recordId = ids[0];
-            }
+    fetchHoldings(): Promise<any> {
+
+        if (this.context.session) {
+            this.context.sessionType = 'mixed';
+            return this.fetchSession(this.context.session);
+
+        } else if (this.context.recordId) {
+            this.context.sessionType = 'record';
+            return this.fetchRecords(this.context.recordId);
+
+        } else if (this.context.volId) {
+            this.context.sessionType = 'vol';
+            return this.fetchVols(this.context.volId);
+
+        } else if (this.context.copyId) {
+            this.context.sessionType = 'copy';
+            return this.fetchCopies(this.context.copyId);
         }
     }
 
-    fetchHoldings(): Promise<any> {
-        if (this.copyId) {
-            return this.fetchCopies(this.copyId);
-        } else if (this.volId) {
-            return this.fetchVols(this.volId);
-        } else if (this.recordId) {
-            return this.fetchRecords(this.recordId);
-        }
+    fetchSession(session: string): Promise<any> {
+
+        return this.cache.getItem(session, 'edit-these-copies')
+        .then((editSession: EditSession) => {
+
+            if (!editSession) { return; }
+
+            this.context.recordId = editSession.record_id;
+            this.context.hideVols = editSession.hide_vols === true;
+            this.context.hideCopies = editSession.hide_copies === true;
+
+            const volsToFetch = [];
+            const volsToCreate = [];
+            editSession.raw.forEach((volData: CallNumData) => {
+                this.context.fastAdd = volData.fast_add === true;
+
+                if (volData.callnumber > 0) {
+                    volsToFetch.push(volData);
+                } else {
+                    volsToCreate.push(volData);
+                }
+            });
+
+            let promise = Promise.resolve();
+            if (volsToFetch.length > 0) {
+                promise = promise.then(_ =>
+                    this.fetchVolsStubCopies(volsToFetch));
+            }
+
+            if (volsToCreate.length > 0) {
+                promise = promise.then(_ =>
+                    this.createVolsStubCopies(volsToCreate));
+            }
+
+            return promise;
+        });
+    }
+
+    // Creating new vols.  Each gets a stub copy.
+    createVolsStubCopies(volDataList: CallNumData[]): Promise<any> {
+
+        const vols = [];
+        volDataList.forEach(volData => {
+
+            const vol = this.volcopy.createStubVol(
+                this.context.recordId,
+                volData.owner || this.auth.user().ws_ou()
+            );
+
+            if (volData.label) {vol.label(volData.label); }
+
+            volData.callnumber = vol.id(); // wanted by addStubCopies
+            vols.push(vol);
+            this.context.findOrCreateVolNode(vol);
+        });
+
+        return this.addStubCopies(vols, volDataList)
+        .then(_ => this.volcopy.setVolClassLabels(vols));
+    }
+
+    // Fetch vols by ID, but instead of retrieving their copies
+    // add a stub copy to each.
+    fetchVolsStubCopies(volDataList: CallNumData[]): Promise<any> {
+
+        const volIds = volDataList.map(volData => volData.callnumber);
+        const vols = [];
+
+        return this.pcrud.search('acn', {id: volIds})
+        .pipe(tap((vol: IdlObject) => vols.push(vol))).toPromise()
+        .then(_ => this.addStubCopies(vols, volDataList));
+    }
+
+    // Add a stub copy to each vol using data from the edit session.
+    addStubCopies(vols: IdlObject[], volDataList: CallNumData[]): Promise<any> {
+
+        const copies = [];
+        vols.forEach(vol => {
+            const volData = volDataList.filter(
+                volData => volData.callnumber === vol.id())[0];
+
+            const copy =
+                this.volcopy.createStubCopy(vol, {circLib: volData.owner});
+
+            this.context.findOrCreateCopyNode(copy);
+            copies.push(copy);
+        });
+
+        return this.volcopy.setCopyStatus(copies, this.context.fastAdd);
     }
 
 
@@ -97,18 +211,18 @@ export class VolCopyComponent implements OnInit {
         .toPromise();
     }
 
-    // Fetch call numbers and copies by call number ids.
+    // Fetch call numbers and linked copies by call number ids.
     fetchVols(volIds?: number | number[]): Promise<any> {
         const ids = [].concat(volIds);
 
         return this.pcrud.search('acn', {id: ids})
         .pipe(tap(vol => this.context.findOrCreateVolNode(vol)))
-        .pipe(tap(vol => {
+        .toPromise().then(_ => {
              return this.pcrud.search('acp',
                 {call_number: ids, deleted: 'f'}, COPY_FLESH
             ).pipe(tap(copy => this.context.findOrCreateCopyNode(copy))
             ).toPromise();
-        })).toPromise();
+        });
     }
 
     // Fetch call numbers and copies by record ids.
@@ -116,10 +230,102 @@ export class VolCopyComponent implements OnInit {
         const ids = [].concat(recordIds);
 
         return this.pcrud.search('acn',
-            {record: ids, deleted: 'f'},
+            {record: ids, deleted: 'f', label: {'!=' : '##URI##'}},
             {}, {idlist: true, atomic: true}
         ).toPromise().then(volIds =>this.fetchVols(volIds));
     }
+
+
+    save() {
+        this.loadingProgress.reset();
+        this.loading = true;
+
+        // Volume update API wants volumes fleshed with copies, instead
+        // of the other way around, which is what we have here.
+        const volumes: IdlObject[] = [];
+
+        this.context.volNodes().forEach(volNode => {
+            const newVol = this.idl.clone(volNode.target);
+            const copies: IdlObject[] = [];
+
+            volNode.children.forEach(copyNode => {
+                const copy = copyNode.target;
+
+                if (copy.isnew() && !copy.barcode()) {
+                    // A new copy w/ no barcode is a stub copy sitting
+                    // on an empty call number.  Ignore it.
+                    return;
+                }
+
+                if (copy.ischanged() || copy.isnew() || copy.isdeleted()) {
+                    const copyClone = this.idl.clone(copy);
+                    // De-flesh call number
+                    copyClone.call_number(copy.call_number().id());
+                    copies.push(copyClone);
+                }
+            });
+
+            newVol.copies(copies);
+
+            if (newVol.ischanged() || newVol.isnew() || copies.length > 0) {
+                volumes.push(newVol);
+            }
+        });
+
+        this.context.volsToDelete.forEach(vol => {
+            const cloneVol = this.idl.clone(vol);
+            // If a deleted volume also has deleted copies, they will
+            // be appended below.
+            cloneVol.copies([]);
+            volumes.push(cloneVol);
+        });
+
+        this.context.copiesToDelete.forEach(copy => {
+            const cloneCopy = this.idl.clone(copy);
+            const copyVol = cloneCopy.call_number();
+            cloneCopy.call_number(copyVol.id()); // de-flesh
+
+            let vol = volumes.filter(v => v.id() === copyVol.id())[0];
+
+            if (vol) {
+                vol.copies().push(cloneCopy);
+            } else {
+                vol = this.idl.clone(copyVol);
+                vol.copies([cloneCopy]);
+            }
+
+            volumes.push(vol);
+        })
+
+        if (volumes.length > 0) {
+            this.saveApi(volumes);
+        } else {
+            this.loading = false;
+        }
+    }
+
+    saveApi(volumes: IdlObject[], override?: boolean) {
+
+        let method = 'open-ils.cat.asset.volume.fleshed.batch.update';
+        if (override) { method += '.override'; }
+
+        this.net.request('open-ils.cat', method, this.auth.token(),
+            volumes, 1, {auto_merge_vols: 1, create_parts: 1}).toPromise()
+
+        .then(resp => {
+
+            const evt = this.evt.parse(resp);
+
+            // TODO: confirm / handle overrides
+
+            if (evt) {
+                alert(evt);
+                return;
+            }
+
+            return this.load();
+        });
+    }
 }
 
 
index cdd9e19..af55152 100644 (file)
@@ -5,6 +5,7 @@ import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
 import {VolCopyRoutingModule} from './routing.module';
 import {VolCopyComponent} from './volcopy.component';
 import {VolEditComponent} from './vol-edit.component';
+import {VolCopyService} from './volcopy.service';
 
 @NgModule({
   declarations: [
@@ -18,6 +19,7 @@ import {VolEditComponent} from './vol-edit.component';
     VolCopyRoutingModule
   ],
   providers: [
+    VolCopyService
   ]
 })
 
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.service.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.service.ts
new file mode 100644 (file)
index 0000000..21d5302
--- /dev/null
@@ -0,0 +1,194 @@
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs';
+import {map, tap, mergeMap} from 'rxjs/operators';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {EventService, EgEvent} from '@eg/core/event.service';
+import {AuthService} from '@eg/core/auth.service';
+import {VolCopyContext} from './volcopy';
+import {HoldingsService, CallNumData} from '@eg/staff/share/holdings/holdings.service';
+
+/* Managing volcopy data */
+
+@Injectable()
+export class VolCopyService {
+
+    autoId = -1;
+
+    constructor(
+        private evt: EventService,
+        private net: NetService,
+        private idl: IdlService,
+        private org: OrgService,
+        private auth: AuthService,
+        private holdings: HoldingsService
+    ) {}
+
+
+    // Fetch vol labels for a single record
+    fetchRecordVolLabels(id: number): Promise<string[]> {
+        if (!id) { return Promise.resolve([]); }
+
+        // NOTE: see https://bugs.launchpad.net/evergreen/+bug/1874897
+        // for more on MARC call numbers and classification scheme.
+        return this.net.request(
+            'open-ils.cat',
+            'open-ils.cat.biblio.record.marc_cn.retrieve', id
+        ).toPromise().then(res => {
+            return Object.values(res)
+                .map(blob => Object.values(blob)[0]).sort();
+        });
+    }
+
+    createStubVol(recordId: number, orgId: number): IdlObject {
+
+        const vol = this.idl.create('acn');
+        vol.id(this.autoId--);
+        vol.isnew(true);
+        vol.record(recordId);
+        vol.label(null);
+        vol.owning_lib(Number(orgId));
+
+        return vol;
+    }
+
+    createStubCopy(vol: IdlObject, options?: any): IdlObject {
+        if (!options) { options = {}; }
+
+        const copy = this.idl.create('acp');
+        copy.id(this.autoId--);
+        copy.isnew(true);
+        copy.circ_lib(Number(options.circLib || vol.owning_lib()));
+        copy.call_number(vol);
+        copy.deposit(0);
+        copy.price(0);
+        copy.deposit_amount(0);
+        copy.fine_level(2);     // Normal
+        copy.loan_duration(2);  // Normal
+        copy.location(1);       // Stacks
+        copy.circulate('t');
+        copy.holdable('t');
+        copy.opac_visible('t');
+        copy.ref('f');
+        copy.mint_condition('t');
+        copy.parts([]);
+
+        // TODO: defaults?
+
+        return copy;
+    }
+
+
+    // Applies label_class values to a batch of volumes, followed by
+    // applying labels to vols that need it.
+    setVolClassLabels(vols: IdlObject[]): Promise<any> {
+
+        const orgIds: any = {};
+        vols.forEach(vol => orgIds[vol.owning_lib()] = true);
+
+        // Serialize
+        let promise = Promise.resolve();
+
+        // TODO: if there is a local default value (ws setting?)
+        // apply it here and bypass the network call.
+
+        const volsWantLabels = [];
+        Object.keys(orgIds).forEach(orgId => {
+            promise = promise.then(_ => {
+
+                return this.org.settings(
+                    'cat.default_classification_scheme', Number(orgId))
+                .then(sets => {
+
+                    const orgVols = vols.filter(v => v.owning_lib() === orgId);
+                    orgVols.forEach(vol => {
+                        vol.label_class(
+                            sets['cat.default_classification_scheme'] || 1
+                        );
+                        if (!vol.label()) { volsWantLabels.push(vol); }
+                    });
+                });
+            });
+        });
+
+        return promise;
+    }
+
+    // Apply labels to volumes based on the appropriate MARC call number.
+    applyVolLabels(vols: IdlObject[]): Promise<any> {
+
+        // Serialize
+        let promise = Promise.resolve();
+
+        vols.forEach(vol => {
+
+            // Avoid unnecessary lookups.
+            // Note the label may have been applied to this volume
+            // in a previous iteration of this loop.
+            if (vol.label()) { return; }
+
+            promise = promise.then(_ => {
+                return this.net.request(
+                    'open-ils.cat',
+                    'open-ils.cat.biblio.record.marc_cn.retrieve',
+                    vol.record(), vol.label_class()).toPromise()
+
+                .then(cnList => {
+                    // Use '_' as a placeholder to indicate when a
+                    // vol has already been addressed.
+                    let label = '_';
+
+                    if (cnList.length > 0) {
+                        const field = Object.keys(cnList[0])[0];
+                        label = cnList[0][field];
+                    }
+
+                    // Avoid making duplicate marc_cn calls by applying
+                    // the label to all vols that apply.
+                    vols.forEach(vol2 => {
+                        if (vol2.record() === vol.record() &&
+                            vol2.label_class() === vol.label_class()) {
+                            vol.label(label);
+                        }
+                    });
+                });
+            });
+        });
+
+        return promise.then(_ => {
+            // Remove the placeholder label
+            vols.forEach(vol => {
+                if (vol.label() === '_') { vol.label(''); }
+            });
+        });
+    }
+
+    // Sets the default copy status for a batch of copies.
+    setCopyStatus(copies: IdlObject[], fastAdd: boolean): Promise<any> {
+
+        const setting = fastAdd ?
+            'cat.default_copy_status_fast' :
+            'cat.default_copy_status_normal'
+
+        const orgIds: any = {};
+        copies.forEach(copy => orgIds[copy.circ_lib()] = true);
+
+        let promise = Promise.resolve();
+        Object.keys(orgIds).forEach(orgId => {
+
+            promise = promise.then(_ =>
+                this.org.settings(setting, Number(orgId))
+            ).then(sets => {
+                // 0 == Available; 5 == In Process
+                const stat = sets[setting] || (fastAdd ? 0 : 5);
+                const orgCopies =
+                    copies.filter(copy => copy.circ_lib() === orgId);
+                orgCopies.forEach(copy => copy.status(stat));
+            });
+        });
+
+        return promise;
+    }
+}
index aa07966..5d745fb 100644 (file)
@@ -1,6 +1,9 @@
 import {IdlObject} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
 
+/* Models the holdings tree and manages related data shared
+ * volcopy across components. */
+
 export class HoldingsTreeNode {
     children: HoldingsTreeNode[];
     nodeType: 'org' | 'vol' | 'copy';
@@ -20,12 +23,35 @@ class HoldingsTree {
 
 export class VolCopyContext {
 
-    autoId = -1;
     holdings: HoldingsTree = new HoldingsTree();
     org: OrgService; // injected
 
+    sessionType: 'copy' | 'vol' | 'record' | 'mixed';
+
+    // Edit content comes from a cached session
+    session: string;
+
+    // Note in multi-record mode this value will be unset.
+    recordId: number;
+
+    // Load specific call number by ID.
+    volId: number;
+
+    // Load specific copy by ID.
+    copyId: number;
+
+    fastAdd: boolean;
+
+    volsToDelete: IdlObject[] = [];
+    copiesToDelete: IdlObject[] = [];
+
+    hideVols: boolean;
+    hideCopies: boolean;
+
     reset() {
         this.holdings = new HoldingsTree();
+        this.volsToDelete = [];
+        this.copiesToDelete = [];
     }
 
     orgNodes(): HoldingsTreeNode[] {
@@ -50,14 +76,23 @@ export class VolCopyContext {
     // Returns IDs for all bib records represented in our holdings tree.
     getRecordIds(): number[] {
         const idHash: {[id: number]: boolean} = {};
-        this.orgNodes().forEach(orgNode => {
-            orgNode.children.forEach(
-                volNode => idHash[volNode.target.record()] = true)
-        });
+
+        this.volNodes().forEach(volNode =>
+            idHash[volNode.target.record()] = true);
 
         return Object.keys(idHash).map(id => Number(id));
     }
 
+    // When working on exactly one record, set our recordId value.
+    setRecordId() {
+        if (!this.recordId) {
+            const ids = this.getRecordIds();
+            if (ids.length === 1) {
+                this.recordId = ids[0];
+            }
+        }
+    }
+
     // Adds an org unit node; unsorted.
     findOrCreateOrgNode(orgId: number): HoldingsTreeNode {
 
@@ -97,7 +132,6 @@ export class VolCopyContext {
 
 
     findOrCreateCopyNode(copy: IdlObject): HoldingsTreeNode {
-
         const volNode = this.findOrCreateVolNode(copy.call_number());
 
         const existing = volNode.children.filter(
@@ -115,7 +149,6 @@ export class VolCopyContext {
         return node;
     }
 
-
     sortHoldings() {
 
         this.orgNodes().forEach(orgNode => {
@@ -137,21 +170,11 @@ export class VolCopyContext {
             o1.target.shortname() < o2.target.shortname() ? -1 : 1);
     }
 
-    // Sorted list of holdings tree nodes
-    /*
-    flattenHoldings(): HoldingsTreeNode[] {
-        this.sortHoldings();
-        let nodes: HoldingsTreeNode[] = [];
+    isSaveable(): boolean {
+        const dupeBc = this.copyList().filter(c => c._dupe_barcode).length;
 
-        this.orgNodes().forEach(orgNode => {
-            nodes.push(orgNode);
-            orgNode.children.forEach(volNode => {
-                nodes.push(volNode);
-                nodes = nodes.concat(volNode.children);
-            });
-        });
+        if (dupeBc) { return false; }
 
-        return nodes;
+        return true;
     }
-    */
 }
index c44a7f7..98c2eb6 100644 (file)
@@ -250,6 +250,8 @@ export class SandboxComponent implements OnInit {
             const query: any = new Array();
             query.push(base);
 
+            console.log(JSON.stringify(this.eventsDataSource.filters));
+
             Object.keys(this.eventsDataSource.filters).forEach(key => {
                 Object.keys(this.eventsDataSource.filters[key]).forEach(key2 => {
                     query.push(this.eventsDataSource.filters[key][key2]);
index 6b42543..8044b5a 100644 (file)
@@ -9,11 +9,12 @@ import {AuthService} from '@eg/core/auth.service';
 import {EventService} from '@eg/core/event.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 
-interface NewCallNumData {
+export interface CallNumData {
     owner?: number;
     label?: string;
     fast_add?: boolean;
     barcode?: string;
+    callnumber?: number;
 }
 
 @Injectable()
@@ -35,7 +36,7 @@ export class HoldingsService {
     spawnAddHoldingsUi(
         recordId: number,               // Bib record ID
         addToCallNums?: number[],       // Add copies to / modify existing CNs
-        callNumData?: NewCallNumData[], // Creating new call numbers
+        callNumData?: CallNumData[], // Creating new call numbers
         hideCopies?: boolean) {         // Hide the copy edit pane
 
         const raw: any[] = [];
@@ -59,7 +60,7 @@ export class HoldingsService {
                 return;
             }
             setTimeout(() => {
-                const url = `/eg/staff/cat/volcopy/${key}`;
+                const url = `/eg2/staff/cat/volcopy/edit/session/${key}`;
                 window.open(url, '_blank');
             });
         });
index 491b6ca..0d57d86 100644 (file)
@@ -1378,7 +1378,8 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
             }
         ).then(function(key) {
             if (key) {
-                var url = egCore.env.basePath + 'cat/volcopy/' + key;
+                //var url = egCore.env.basePath + 'cat/volcopy/' + key;
+                var url = '/eg2/staff/cat/volcopy/session/' + key;
                 $timeout(function() { $window.open(url, '_blank') });
             } else {
                 alert('Could not create anonymous cache key!');