LPXXX Angular Volcopy
authorBill Erickson <berickxx@gmail.com>
Thu, 18 Jun 2020 16:24:53 +0000 (12:24 -0400)
committerBill Erickson <berickxx@gmail.com>
Thu, 18 Jun 2020 16:24:53 +0000 (12:24 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
Open-ILS/src/eg2/src/app/share/item-location-select/item-location-select.component.ts
Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.html
Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.ts
Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.ts

index bfbeb4d..d4dd6f1 100644 (file)
@@ -35,6 +35,7 @@ export interface ComboboxEntry {
   }]
 })
 export class ComboboxComponent implements ControlValueAccessor, OnInit {
+    static domIdAuto = 0;
 
     selected: ComboboxEntry;
     click$: Subject<string>;
@@ -42,7 +43,6 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit {
 
     @ViewChild('instance', { static: true }) instance: NgbTypeahead;
 
-    static domIdAuto = 0;
     @Input() domId = 'eg-combobox-' + ComboboxComponent.domIdAuto++;
 
     // Applies a name attribute to the input.
index 2ea5ef8..d5a3baf 100644 (file)
@@ -30,6 +30,7 @@ import {StringComponent} from '@eg/share/string/string.component';
 })
 export class ItemLocationSelectComponent
     implements OnInit, AfterViewInit, ControlValueAccessor {
+    static domIdAuto = 0;
 
     // Limit copy locations to those owned at or above org units where
     // the user has work permissions for the provided permission code.
@@ -45,7 +46,6 @@ export class ItemLocationSelectComponent
 
     @Input() required: boolean;
 
-    static domIdAuto = 0;
     @Input() domId = 'eg-item-location-select-' +
         ItemLocationSelectComponent.domIdAuto++;
 
index 4336e56..431ac60 100644 (file)
@@ -24,6 +24,7 @@ interface OrgDisplay {
   templateUrl: './org-select.component.html'
 })
 export class OrgSelectComponent implements OnInit {
+    static domIdAuto = 0;
 
     selected: OrgDisplay;
     hidden: number[] = [];
@@ -40,7 +41,6 @@ export class OrgSelectComponent implements OnInit {
     @Input() stickySetting: string;
 
     // ID to display in the DOM for this selector
-    static domIdAuto = 0;
     @Input() domId = 'eg-org-select-' + OrgSelectComponent.domIdAuto++;
 
     // Org unit field displayed in the selector
index eae85ec..9dcccf5 100644 (file)
   <!-- COLUMN 5 -->
   <div class="flex-1 p-1">
     <div class="p-1"><h4 class="font-weight-bold" i18n>Statistics</h4></div>
+
+    <div *ngFor="let cat of statCats; let idx = index">
+      <ng-template #statCatTemplate>
+        <select class="form-control" id="stat-cat-input-{{idx}}" 
+          [(ngModel)]="statCatValues[cat.id()]">
+          <option *ngFor="let entry of cat.entries()" 
+            value="{{entry.id()}}" i18n>{{entry.value()}}</option>
+        </select>
+      </ng-template>
+      <eg-batch-item-attr label="{{cat.name()}} ({{orgSn(cat.owner())}})" i18n-label
+        editInputDomId="stat-cat-input-{{idx}}"
+        [emptyIsUnset]="true"
+        [editTemplate]="statCatTemplate"
+        [labelCounts]="statCatCounts(cat.id())"
+        (changesSaved)="statCatChanged(cat.id())">
+      </eg-batch-item-attr>
+    </div>
+
   </div>
 </div>
 
index ccfe284..b955cee 100644 (file)
@@ -25,10 +25,15 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
     // Some values are scalar, some IdlObjects depending on copy fleshyness.
     values: {[field: string]: any} = {};
 
+    // Map of stat ID to entry ID.
+    statCatValues: {[statId: number]: number} = {};
+
     ageProtectRules: IdlObject[] = [];
     floatingGroups: IdlObject[] = [];
     itemTypeMaps: IdlObject[] = [];
     circModifiers: IdlObject[] = [];
+    statCats: IdlObject[] = [];
+    statCatEntryMap: {[id: number]: IdlObject} = {}; // entry id => entry
 
     loanDurationLabelMap: {[level: number]: string} = {};
     fineLevelLabelMap: {[level: number]: string} = {};
@@ -94,7 +99,7 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
         }).then(_ => {
 
             return this.pcrud.retrieveAll('cfg')
-            .pipe(tap(rule => this.floatingGroups.push(rule))).toPromise()
+            .pipe(tap(rule => this.floatingGroups.push(rule))).toPromise();
 
         }).then(_ => {
 
@@ -104,7 +109,7 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
         }).then(_ => {
 
             return this.pcrud.retrieveAll('ccm')
-            .pipe(tap(rule => this.circModifiers.push(rule))).toPromise()
+            .pipe(tap(rule => this.circModifiers.push(rule))).toPromise();
 
         }).then(_ => {
 
@@ -114,13 +119,72 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
         }).then(_ => {
 
             return this.pcrud.retrieveAll('citm')
-            .pipe(tap(itemType => this.itemTypeMaps.push(itemType))).toPromise()
+            .pipe(tap(itemType => this.itemTypeMaps.push(itemType))).toPromise();
 
         }).then(_ => {
 
             this.itemTypeMaps = this.itemTypeMaps.sort(
                 (a, b) => a.value() < b.value() ? -1 : 1);
+
+        }).then(_ => {
+
+            return this.net.request('open-ils.circ',
+                'open-ils.circ.stat_cat.asset.retrieve.all',
+                this.auth.token(), this.auth.user().ws_ou()
+            ).toPromise().then(stats => this.statCats = stats);
+
+        }).then(_ => {
+
+            // Sort most local to the front of the list.
+            this.statCats = this.statCats.sort((s1, s2) => {
+                const d1 = this.org.get(s1.owner()).ou_type().depth();
+                const d2 = this.org.get(s2.owner()).ou_type().depth();
+
+                if (d1 > d2) {
+                    return -1;
+                } else if (d1 < d2) {
+                    return 1;
+                } else {
+                    return s1.name() < s2.name() ? -1 : 1;
+                }
+            });
+
+            this.statCats.forEach(cat => {
+                cat.entries().forEach(
+                    entry => this.statCatEntryMap[entry.id()] = entry);
+            });
+        });
+    }
+
+    orgSn(orgId: number): string {
+        return orgId ? this.org.get(orgId).shortname() : '';
+    }
+
+    statCatCounts(catId: number): {[value: string]: number} {
+        catId = Number(catId);
+        const counts = {};
+
+        this.context.copyList().forEach(copy => {
+            const entry = copy.stat_cat_entries()
+                .filter(e => e.stat_cat() === catId)[0];
+
+            let value = '';
+            if (entry) {
+                if (this.statCatEntryMap[entry.id()]) {
+                    value = this.statCatEntryMap[entry.id()].value();
+                } else {
+                    // Map to a remote stat cat.  Ignore.
+                    return;
+                }
+            }
+
+            if (counts[value] === undefined) {
+                counts[value] = 0;
+            }
+            counts[value]++;
         });
+
+        return counts;
     }
 
     itemAttrCounts(field: string): {[value: string]: number} {
@@ -131,7 +195,7 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
 
             if (counts[value] === undefined) {
                 counts[value] = 0;
-            };
+            }
             counts[value]++;
         });
 
@@ -147,11 +211,11 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
                 ' : ' + copy.call_number().label();
         }
 
-        let value = copy[field]();
+        const value = copy[field]();
 
         if (!value && value !== 0) { return ''; }
 
-        switch(field) {
+        switch (field) {
 
             case 'status':
                 return this.volcopy.copyStatuses[value].name();
@@ -228,8 +292,41 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit {
 
     owningLibChanged() {
         // TODO
+        // copies.ischanged(true);
         console.log('OWNING LIB ', this.values['owning_lib']);
     }
+
+
+    // Create or modify a stat cat entry for each copy that does not
+    // already match the new value.
+    statCatChanged(catId: number) {
+        catId = Number(catId);
+
+        const entryId = this.statCatValues[catId];
+        this.context.copyList().forEach(copy => {
+
+            let entry = copy.stat_cat_entries()
+                .filter(e => e.stat_cat() === catId)[0];
+
+            if (entry) {
+                if (entry.id() === entryId) {
+                    // Requested mapping already exists.
+                    return;
+                }
+            } else {
+
+                // Copy has no entry for this stat cat yet.
+                entry = this.idl.create('asce');
+                entry.stat_cat(catId);
+                copy.stat_cat_entries().push(entry);
+            }
+
+            entry.id(entryId);
+            entry.value(this.statCatEntryMap[entryId].value());
+
+            copy.ischanged(true);
+        });
+    }
 }
 
 
index c5da3cc..44f6db4 100644 (file)
@@ -379,7 +379,7 @@ export class VolEditComponent implements OnInit {
     }
 
     barcodeChanged(copy: IdlObject, barcode: string) {
-        copy.barcode(barcode);
+        // note: copy.barcode(barcode) applied via ngModel
         copy.ischanged(true);
         copy._dupe_barcode = false;
 
@@ -412,7 +412,6 @@ export class VolEditComponent implements OnInit {
     }
 
     deleteOneCopy(copyNode: HoldingsTreeNode) {
-
         const targetCopy = copyNode.target;
 
         const orgNodes = this.context.orgNodes();
@@ -462,6 +461,7 @@ export class VolEditComponent implements OnInit {
     }
 
     deleteOneVol(volNode: HoldingsTreeNode) {
+
         let deleteVolIdx = null;
         const targetVol = volNode.target;
 
index 5932ffb..7d493f1 100644 (file)
@@ -16,7 +16,8 @@ import {VolCopyService} from './volcopy.service';
 const COPY_FLESH = {
     flesh: 1,
     flesh_fields: {
-        acp: ['call_number', 'location', 'parts', 'creator', 'editor']
+        acp: ['call_number', 'location', 'parts',
+                'creator', 'editor', 'stat_cat_entries']
     }
 };
 
@@ -317,6 +318,17 @@ export class VolCopyComponent implements OnInit {
             volumes.push(vol);
         });
 
+        // De-flesh before posting
+        volumes.forEach(vol => {
+            vol.copies().forEach(copy => {
+                ['editor', 'creator', 'location'].forEach(field => {
+                    if (typeof copy[field]() === 'object') {
+                        copy[field](copy[field]().id());
+                    }
+                });
+            });
+        });
+
         if (volumes.length > 0) {
             this.saveApi(volumes);
         } else {
index 5d745fb..d751c7e 100644 (file)
@@ -170,11 +170,24 @@ export class VolCopyContext {
             o1.target.shortname() < o2.target.shortname() ? -1 : 1);
     }
 
+    // Changes pending and no unresolved issues.
     isSaveable(): boolean {
         const dupeBc = this.copyList().filter(c => c._dupe_barcode).length;
 
         if (dupeBc) { return false; }
 
-        return true;
+        const modified = (o: IdlObject): boolean => {
+            return o.isnew() || o.ischanged() || o.isdeleted();
+        };
+
+        if (this.volNodes().filter(n => modified(n.target)).length > 0) {
+            return true;
+        }
+
+        if (this.copyList().filter(c => modified(c)).length > 0) {
+            return true;
+        }
+
+        return false;
     }
 }