From: Bill Erickson Date: Tue, 16 Jun 2020 14:47:49 +0000 (-0400) Subject: LPXXX Angular Volcopy X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=6b580f6023017c88379f4c9d53782a481a196efc;p=working%2FEvergreen.git LPXXX Angular Volcopy Signed-off-by: Bill Erickson --- 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 ce3fcacf18..20abe09816 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 @@ -10,6 +10,7 @@ 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', @@ -39,7 +40,6 @@ export class VolEditComponent implements OnInit { batchVolSuffix: ComboboxEntry; batchVolLabel: ComboboxEntry; - autoBarcodeInProgress = false; useCheckdigit = false; @@ -48,6 +48,8 @@ export class VolEditComponent implements OnInit { deleteVolCount: number = null; deleteCopyCount: number = null; + recordVolLabels: string[] = []; + @ViewChild('confirmDelVol', {static: false}) confirmDelVol: ConfirmDialogComponent; @@ -60,7 +62,8 @@ export class VolEditComponent implements OnInit { private pcrud: PcrudService, private net: NetService, private auth: AuthService, - private holdings: HoldingsService + private holdings: HoldingsService, + private volcopy: VolCopyService ) {} ngOnInit() { @@ -68,7 +71,8 @@ export class VolEditComponent implements OnInit { this.deleteVolCount = null; this.deleteCopyCount = null; - this.context.fetchRecordVolLabels() + this.volcopy.fetchRecordVolLabels(this.context.recordId) + .then(labels => this.recordVolLabels = labels) .then(_ => this.fetchBibParts()) .then(_ => this.addStubCopies()); @@ -179,7 +183,7 @@ export class VolEditComponent implements OnInit { // Our context assumes copies are fleshed with volumes const vol = volNode.target; - const copy = this.context.createStubCopy(vol); + const copy = this.volcopy.createStubCopy(vol); copy.call_number(vol); this.context.findOrCreateCopyNode(copy); } @@ -190,10 +194,11 @@ export class VolEditComponent implements OnInit { for (let i = 0; i < count; i++) { // This will vivify the volNode if needed. - const vol = this.context.createStubVol(orgNode.target.id()) + const vol = this.volcopy.createStubVol( + this.context.recordId, orgNode.target.id()) // Our context assumes copies are fleshed with volumes - const copy = this.context.createStubCopy(vol); + const copy = this.volcopy.createStubCopy(vol); copy.call_number(vol); this.context.findOrCreateCopyNode(copy); } @@ -219,7 +224,7 @@ export class VolEditComponent implements OnInit { nodes.forEach(volNode => { if (volNode.children.length == 0) { const vol = volNode.target; - const copy = this.context.createStubCopy(vol); + const copy = this.volcopy.createStubCopy(vol); copy.call_number(vol); this.context.findOrCreateCopyNode(copy); } diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.html b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.html index 0de85f8647..2f569ea94c 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.html +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.html @@ -19,9 +19,9 @@
+ [disabled]="!context.isSaveable()" (click)="save()" i18n>Save
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.ts index 68e53cdae8..cddbfc9416 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.ts @@ -11,6 +11,7 @@ import {HoldingsService, CallNumData} from '@eg/staff/share/holdings/holdings.se 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, @@ -34,18 +35,14 @@ interface EditSession { hide_copies: boolean; } - @Component({ templateUrl: 'volcopy.component.html' }) export class VolCopyComponent implements OnInit { context: VolCopyContext; - - hideVols: boolean; - hideCopies: boolean; - loading = true; + @ViewChild('loadingProgress', {static: false}) loadingProgress: ProgressInlineComponent; @@ -60,14 +57,13 @@ export class VolCopyComponent implements OnInit { private auth: AuthService, private pcrud: PcrudService, private cache: AnonCacheService, - private holdings: HoldingsService + private holdings: HoldingsService, + private volcopy: VolCopyService ) { } ngOnInit() { this.context = new VolCopyContext(); this.context.org = this.org; // inject; - this.context.idl = this.idl; // inject; - this.context.net = this.net; // inject; this.route.paramMap.subscribe( (params: ParamMap) => this.negotiateRoute(params)); @@ -82,44 +78,35 @@ export class VolCopyComponent implements OnInit { } load() { + this.loading = true; this.context.reset(); + this.fetchHoldings() - .then(_ => this.loadingProgress.increment()) - .then(_ => this.context.applyVolLabels()) - .then(_ => this.loadingProgress.increment()) + .then(_ => this.volcopy.applyVolLabels( + this.context.volNodes().map(n => n.target))) .then(_ => this.holdings.fetchCallNumberClasses()) - .then(_ => this.loadingProgress.increment()) .then(_ => this.holdings.fetchCallNumberPrefixes()) - .then(_ => this.loadingProgress.increment()) .then(_ => this.holdings.fetchCallNumberSuffixes()) - .then(_ => this.loadingProgress.increment()) .then(_ => this.context.sortHoldings()) - .then(_ => this.loadingProgress.increment()) - .then(_ => this.setRecordId()) - .then(_ => this.loadingProgress.increment()) + .then(_ => this.context.setRecordId()) .then(_ => this.loading = false); } - setRecordId() { - if (!this.context.recordId) { - const ids = this.context.getRecordIds(); - if (ids.length === 1) { - this.context.recordId = ids[0]; - } - } - } - fetchHoldings(): Promise { + 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); @@ -133,11 +120,9 @@ export class VolCopyComponent implements OnInit { if (!editSession) { return; } - console.log('EDIT SES', editSession); - this.context.recordId = editSession.record_id; - this.hideVols = editSession.hide_vols === true; - this.hideCopies = editSession.hide_copies === true; + this.context.hideVols = editSession.hide_vols === true; + this.context.hideCopies = editSession.hide_copies === true; const volsToFetch = []; const volsToCreate = []; @@ -166,14 +151,15 @@ export class VolCopyComponent implements OnInit { }); } + // Creating new vols. Each gets a stub copy. createVolsStubCopies(volDataList: CallNumData[]): Promise { const vols = []; volDataList.forEach(volData => { - const vol = this.context.createStubVol( - volData.owner || this.auth.user().ws_ou(), - this.context.recordId + const vol = this.volcopy.createStubVol( + this.context.recordId, + volData.owner || this.auth.user().ws_ou() ); if (volData.label) {vol.label(volData.label); } @@ -184,7 +170,7 @@ export class VolCopyComponent implements OnInit { }); return this.addStubCopies(vols, volDataList) - .then(_ => this.context.setVolClassLabels(vols)); + .then(_ => this.volcopy.setVolClassLabels(vols)); } // Fetch vols by ID, but instead of retrieving their copies @@ -199,6 +185,7 @@ export class VolCopyComponent implements OnInit { .then(_ => this.addStubCopies(vols, volDataList)); } + // Add a stub copy to each vol using data from the edit session. addStubCopies(vols: IdlObject[], volDataList: CallNumData[]): Promise { const copies = []; @@ -207,13 +194,13 @@ export class VolCopyComponent implements OnInit { volData => volData.callnumber === vol.id())[0]; const copy = - this.context.createStubCopy(vol, {circLib: volData.owner}); + this.volcopy.createStubCopy(vol, {circLib: volData.owner}); this.context.findOrCreateCopyNode(copy); copies.push(copy); }); - return this.context.setCopyStatus(copies); + return this.volcopy.setCopyStatus(copies, this.context.fastAdd); } @@ -224,7 +211,7 @@ 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 { const ids = [].concat(volIds); diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.module.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.module.ts index cdd9e19d16..af55152bc3 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.module.ts @@ -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 index 0000000000..21d5302b5b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.service.ts @@ -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 { + 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 { + + 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 { + + // 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 { + + 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; + } +} 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 b0c490d5ce..5d745fb2a0 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 @@ -1,6 +1,8 @@ -import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {IdlObject} from '@eg/core/idl.service'; import {OrgService} from '@eg/core/org.service'; -import {NetService} from '@eg/core/net.service'; + +/* Models the holdings tree and manages related data shared + * volcopy across components. */ export class HoldingsTreeNode { children: HoldingsTreeNode[]; @@ -21,11 +23,8 @@ class HoldingsTree { export class VolCopyContext { - autoId = -1; holdings: HoldingsTree = new HoldingsTree(); org: OrgService; // injected - idl: IdlService; - net: NetService; sessionType: 'copy' | 'vol' | 'record' | 'mixed'; @@ -46,7 +45,8 @@ export class VolCopyContext { volsToDelete: IdlObject[] = []; copiesToDelete: IdlObject[] = []; - recordVolLabels: string[] = []; + hideVols: boolean; + hideCopies: boolean; reset() { this.holdings = new HoldingsTree(); @@ -76,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 { @@ -123,7 +132,6 @@ export class VolCopyContext { findOrCreateCopyNode(copy: IdlObject): HoldingsTreeNode { - const volNode = this.findOrCreateVolNode(copy.call_number()); const existing = volNode.children.filter( @@ -162,188 +170,11 @@ export class VolCopyContext { o1.target.shortname() < o2.target.shortname() ? -1 : 1); } - dataIsSaveable(): boolean { + isSaveable(): boolean { const dupeBc = this.copyList().filter(c => c._dupe_barcode).length; if (dupeBc) { return false; } return true; } - - // This is the MARC CN label that will appear in the batch selector. - fetchRecordVolLabels(): Promise { - // NOTE: see https://bugs.launchpad.net/evergreen/+bug/1874897 - // for more on MARC call numbers and classification scheme. - - this.recordVolLabels = []; - const ids = this.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(); - }); - } - - createStubVol(orgId: number, recordId?: number): IdlObject { - // Volume creation should only be available as an option when - // working with a specific call bib record. - const recId = recordId || +this.getRecordIds()[0]; - - const vol = this.idl.create('acn'); - vol.id(this.autoId--); - vol.isnew(true); - vol.record(recId); - vol.label(this.recordVolLabels[0] || ''); - 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 { - - 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(): Promise { - - // Serialize - let promise = Promise.resolve(); - const vols = this.volNodes() - .map(volNode => volNode.target).filter(vol => !vol.label()); - - 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[]): Promise { - - const setting = this.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] || (this.fastAdd ? 0 : 5); - const orgCopies = - copies.filter(copy => copy.circ_lib() === orgId); - orgCopies.forEach(copy => copy.status(stat)); - }); - }); - - return promise; - } - }