From: Bill Erickson Date: Tue, 23 Jun 2020 20:57:11 +0000 (-0400) Subject: LPXXX Angular Volcopy X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=0245dec35a2a22407e5f5813aaa72502b6477959;p=working%2FEvergreen.git LPXXX Angular Volcopy Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/config.component.html b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/config.component.html index 93b264c47e..00fbe3636e 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/config.component.html +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/config.component.html @@ -121,10 +121,36 @@
+

Item Attribute Settings

+ +
+
+
+
Item Attributes Behavior
+
    +
  • +
    + + +
    +
  • +
+
+
+
+ + +

Hide Item Attributes

- Selected Attributes Will be Hidden from the Item Attributes Form. + Selected Fields Will be Hidden + from the Item Attributes Form.
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.html b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.html index 4ea94abcb6..e20dcac53e 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.html +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.html @@ -32,17 +32,36 @@
Templates:
- + +
+ + + + + + + + +
- - +
@@ -127,6 +146,7 @@
+ + context:{field:'owning_lib',template:owningLibTemplate,label:olLabel.text}">
@@ -413,6 +433,7 @@ [emptyIsUnset]="true" [editTemplate]="statCatTemplate" [labelCounts]="statCatCounts(cat.id())" + (valueCleared)="statCatChanged(cat.id(), true)" (changesSaved)="statCatChanged(cat.id())"> diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts index 44d2858bbb..bad44c6198 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts @@ -1,6 +1,7 @@ import {Component, Input, OnInit, AfterViewInit, ViewChild, QueryList, ViewChildren} from '@angular/core'; import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {SafeUrl} from '@angular/platform-browser'; import {tap} from 'rxjs/operators'; import {IdlObject, IdlService} from '@eg/core/idl.service'; import {EventService} from '@eg/core/event.service'; @@ -18,6 +19,7 @@ import {CopyAlertsDialogComponent } from '@eg/staff/share/holdings/copy-alerts-dialog.component'; import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component'; import {BatchItemAttrComponent} from '@eg/staff/share/holdings/batch-item-attr.component'; +import {FileExportService} from '@eg/share/util/file-export.service'; @Component({ selector: 'eg-copy-attrs', @@ -83,6 +85,7 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit { private holdings: HoldingsService, private format: FormatService, private store: StoreService, + private fileExport: FileExportService, public volcopy: VolCopyService ) { } @@ -92,7 +95,10 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit { ngAfterViewInit() { const tmpl = this.store.getLocalItem('cat.copy.last_template'); - if (tmpl) { this.copyTemplateCbox.selectedId = tmpl; } + if (tmpl) { + // avoid Express Changed warning w/ timeout + setTimeout(() => this.copyTemplateCbox.selectedId = tmpl); + } this.loanDurationLabelMap[1] = this.loanDurationShort.text; this.loanDurationLabelMap[2] = this.loanDurationNormal.text; @@ -231,9 +237,9 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit { this.values[field] = value; } - // TODO: handle circ_lib, owning_lib changes specially - - console.debug('APPLYING', field, value); + if (field === 'owning_lib') { + return this.owningLibChanged(value); + } this.context.copyList().forEach(copy => { if (copy[field] && copy[field]() !== value) { @@ -243,16 +249,82 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit { }); } + owningLibChanged(orgId: number) { + if (!orgId) { return; } + + let promise = Promise.resolve(); + + // Map existing vol IDs to their replacments. + const newVols: any = {}; + + this.context.copyList().forEach(copy => { + + // Change the copy circ lib to match the new owning lib + // if configured to do so. + if (this.volcopy.defaults.values.circ_lib_mod_with_owning_lib) { + if (copy.circ_lib() !== orgId) { + copy.circ_lib(orgId); + copy.ischanged(true); + + this.batchAttrs + .filter(ba => ba.name === 'circ_lib') + .forEach(attr => attr.hasChanged = true); + } + } + + const vol = copy.call_number(); + + if (vol.owning_lib() === orgId) { return; } // No change needed + + let newVol; + if (newVols[vol.id()]) { + newVol = newVols[vol.id()]; + + } else { + + // The open-ils.cat.asset.volume.fleshed.batch.update API + // will use the existing volume when trying to create a + // new volume with the same parameters as an existing volume. + newVol = this.idl.clone(vol); + newVol.owning_lib(orgId); + newVol.id(this.volcopy.autoId--); + newVol.isnew(true); + newVols[vol.id()] = newVol; + } + + copy.call_number(newVol); + copy.ischanged(); + + this.context.removeCopyNode(copy.id()); + this.context.findOrCreateCopyNode(copy); + }); + + // If any of the above actions results in an empty volume + // remove it from the tree. Note this does not delete the + // volume at the server, since other items could be attached + // of which this instance of the editor is not aware. + Object.keys(newVols).forEach(volId => { + const volNode = this.context.volNodes().filter( + volNode => volNode.target.id() === +volId)[0]; + + if (volNode && volNode.children.length === 0) { + this.context.removeVolNode(+volId); + } + }); + } + // Create or modify a stat cat entry for each copy that does not // already match the new value. - statCatChanged(catId: number) { + statCatChanged(catId: number, clear?: boolean) { 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]; + console.log('0', entry); if (entry) { if (entry.id() === entryId) { @@ -265,6 +337,7 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit { entry = this.idl.create('asce'); entry.stat_cat(catId); copy.stat_cat_entries().push(entry); + console.log('1', entry); } entry.id(entryId); @@ -299,6 +372,8 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit { this.store.setLocalItem('cat.copy.last_template', entry.id); + // TODO: handle owning_lib + const template = this.volcopy.templates[entry.id]; Object.keys(template).forEach(field => { @@ -306,14 +381,128 @@ export class CopyAttrsComponent implements OnInit, AfterViewInit { if (value === null || value === undefined) { return; } - this.applyCopyValue(field, value); + if (field === 'statcats') { + Object.keys(value).forEach(catId => { + this.statCatValues[+catId] = value[+catId]; + this.statCatChanged(+catId); + }); + return; + } + + // In some cases, we may have to fetch the data since + // the local code assumes copy field is fleshed. + let promise = Promise.resolve(value); + + if (field === 'location') { + // May be a 'remote' location. Fetch as needed. + promise = this.volcopy.getLocation(value); + } + + promise.then(val => { + this.applyCopyValue(field, val); + + // Indicate in the form these values have changed + this.batchAttrs + .filter(ba => ba.name === field) + .forEach(attr => attr.hasChanged = true); + }); }); } saveTemplate() { + const entry: ComboboxEntry = this.copyTemplateCbox.selected; + if (!entry) { return; } + + let name; + let template; + + if (entry.freetext) { + name = entry.label; + // freetext entries don't have an ID, but we may need one later. + entry.id = entry.label; + template = {}; + } else { + name = entry.id; + template = this.volcopy.templates[name]; + } + + // Changes will have applied to all items. + const copy = this.context.copyList()[0]; + this.batchAttrs.forEach(comp => { - console.log(comp.editInputDomId); + if (!comp.hasChanged) { return; } + + const value = copy[comp.name](); + const name = comp.name; + + if (value === null) { + delete template[name]; + return; + } + + if (name.match(/stat_cat_/)) { + const statId = name.match(/stat_cat_(\d+)/)[1]; + if (!template.statcats) { template.statcats = {}; } + + template.statcats[statId] = value; + + } else { + + // some values are fleshed. + // this assumes fleshed objects have an 'id' value, + // which is true so far. + template[name] = + typeof value === 'object' ? value.id() : value; + } }); + + this.volcopy.templates[name] = template; + this.volcopy.saveTemplates(); + } + + exportTemplate($event) { + if (this.fileExport.inProgress()) { return; } + + this.fileExport.exportFile( + $event, JSON.stringify(this.volcopy.templates), 'text/json'); + } + + importTemplate($event) { + const file: File = $event.target.files[0]; + if (!file) { return; } + + const reader = new FileReader(); + + reader.addEventListener('load', () => { + + try { + const template = JSON.parse(reader.result as string); + const name = Object.keys(template)[0]; + this.volcopy.templates[name] = template[name]; + } catch (E) { + console.error('Invalid Item Attribute template', E); + return; + } + + this.volcopy.saveTemplates(); + // Adds the new one to the list and re-sorts the labels. + this.volcopy.fetchTemplates(); + }); + + reader.readAsText(file); + } + + // Returns null when no export is in progress. + exportTemplateUrl(): SafeUrl { + return this.fileExport.safeUrl; + } + + deleteTemplate() { + const entry: ComboboxEntry = this.copyTemplateCbox.selected; + if (!entry) { return; } + delete this.volcopy.templates[entry.id]; + this.volcopy.saveTemplates(); + this.copyTemplateCbox.selected = null; } displayAttr(field: string): boolean { diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.ts index b6948a41d7..f01c346d29 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.ts @@ -40,8 +40,6 @@ export class VolEditComponent implements OnInit { autoBarcodeInProgress = false; useCheckdigit = false; - autoId = -1; - deleteVolCount: number = null; deleteCopyCount: number = null; 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 index d9ce8a0594..63081e381e 100644 --- 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 @@ -15,6 +15,8 @@ import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.comp /* Managing volcopy data */ + + interface VolCopyDefaults { values: {[field: string]: any}, hidden: {[field: string]: boolean} @@ -25,10 +27,15 @@ export class VolCopyService { autoId = -1; + localOrgs: number[]; defaults: VolCopyDefaults = null; defaultLocation: IdlObject; copyStatuses: {[id: number]: IdlObject} = null; + // This will be all 'local' copy locations plus any remote + // locations that we are required to interact with. + copyLocationMap: {[id: number]: IdlObject} = {}; + // Track this here so it can survive route changes. currentContext: VolCopyContext; @@ -63,9 +70,10 @@ export class VolCopyService { if (this.itemTypeMaps.length > 0) { return Promise.resolve(); } - const myOrgs = this.org.fullPath(this.auth.user().ws_ou(), true); + this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true); return this.fetchDefaults() + .then(_ => this.getLocations()) .then(_ => this.holdings.fetchCallNumberClasses()) .then(cls => this.volClasses = cls) @@ -73,7 +81,7 @@ export class VolCopyService { return this.holdings.fetchCallNumberPrefixes().then(prefixes => this.volPrefixes = prefixes .filter(sfx => sfx.id() !== -1) - .filter(pfx => myOrgs.includes(pfx.owning_lib())) + .filter(pfx => this.localOrgs.includes(pfx.owning_lib())) ); }) @@ -81,7 +89,7 @@ export class VolCopyService { return this.holdings.fetchCallNumberSuffixes().then(suffixes => this.volSuffixes = suffixes .filter(sfx => sfx.id() !== -1) - .filter(sfx => myOrgs.includes(sfx.owning_lib())) + .filter(sfx => this.localOrgs.includes(sfx.owning_lib())) ); }) @@ -157,6 +165,26 @@ export class VolCopyService { }); } + getLocation(id: number): Promise { + if (this.copyLocationMap[id]) { + return Promise.resolve(this.copyLocationMap[id]); + } + + return this.pcrud.retrieve('acpl', id) + .pipe(tap(loc => this.copyLocationMap[loc.id()] = loc)) + .toPromise(); + } + + getLocations(): Promise { + this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true); + + return this.pcrud.search('acpl', + {deleted: 'f', owning_lib: this.localOrgs}, + {order_by: {acpl: 'name'}} + ).pipe(tap(loc => this.copyLocationMap[loc.id()] = loc) + ).toPromise(); + } + fetchTemplates(): Promise { // TODO: copy templates should be server settings @@ -171,6 +199,14 @@ export class VolCopyService { return Promise.resolve(); } + + saveTemplates(): Promise { + // TODO: templates should be stored on the server. + this.store.setLocalItem('cat.copy.templates', this.templates); + // Re-sort, etc. + return this.fetchTemplates(); + } + fetchDefaults(): Promise { if (this.defaults) { return Promise.resolve(); } @@ -184,9 +220,9 @@ export class VolCopyService { // Use the first non-deleted copy location within org unit // range as the default. Typically "Stacks". - const myOrgs = this.org.fullPath(this.auth.user().ws_ou(), true); + this.localOrgs = this.org.fullPath(this.auth.user().ws_ou(), true); return this.pcrud.search('acpl', - {deleted: 'f', owning_lib: myOrgs}, + {deleted: 'f', owning_lib: this.localOrgs}, {order_by: {acpl: 'id'}, limit: 1} ).toPromise().then(loc => this.defaultLocation = loc); }); diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.ts index d751c7eda5..b47b59dd5d 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.ts +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.ts @@ -149,6 +149,28 @@ export class VolCopyContext { return node; } + removeVolNode(volId: number) { + this.orgNodes().forEach(orgNode => { + for (let idx = 0; idx < orgNode.children.length; idx++) { + if (orgNode.children[idx].target.id() === volId) { + orgNode.children.splice(idx, 1); + break; + } + } + }); + } + + removeCopyNode(copyId: number) { + this.volNodes().forEach(volNode => { + for (let idx = 0; idx < volNode.children.length; idx++) { + if (volNode.children[idx].target.id() === copyId) { + volNode.children.splice(idx, 1); + break; + } + } + }); + } + sortHoldings() { this.orgNodes().forEach(orgNode => { diff --git a/Open-ILS/src/eg2/src/app/staff/common.module.ts b/Open-ILS/src/eg2/src/app/staff/common.module.ts index 85dd7fcd4b..f6f4140d1b 100644 --- a/Open-ILS/src/eg2/src/app/staff/common.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/common.module.ts @@ -18,6 +18,7 @@ import {MultiSelectComponent} from '@eg/share/multi-select/multi-select.componen import {NotBeforeMomentValidatorDirective} from '@eg/share/validators/not_before_moment_validator.directive'; import {PatronBarcodeValidatorDirective} from '@eg/share/validators/patron_barcode_validator.directive'; import {BroadcastService} from '@eg/share/util/broadcast.service'; +import {FileExportService} from '@eg/share/util/file-export.service'; /** * Imports the EG common modules and adds modules common to all staff UI's. @@ -71,7 +72,8 @@ export class StaffCommonModule { providers: [ // Export staff-wide services AccessKeyService, AudioService, - BroadcastService + BroadcastService, + FileExportService ] }; }