From: Bill Erickson Date: Fri, 5 Jun 2020 20:54:23 +0000 (-0400) Subject: LPXXX Angular Volcopy X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=85c23377e8c726b0c8687704e4b062ac62038743;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.css b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.css index c109cac77e..fabadae7d7 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.css +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.css @@ -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; +} + diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.html b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.html index 42fd35465c..673c9c7759 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.html +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.html @@ -1,31 +1,42 @@ + + + + +
-
-
-
-
+
+
- +
-
- +
-
- +
@@ -34,19 +45,19 @@
- +
-
- +
-
- -
+
-
-
+
+
+
+ + +
+
@@ -105,24 +124,34 @@
- {{orgNode.target.shortname()}} + + {{orgNode.target.shortname()}} + {{sessionType}} + + + +
- +
- - @@ -130,12 +159,12 @@
- - @@ -147,17 +176,17 @@ spellcheck="false" [required]="true" [ngModel]="volNode.target.label()" - (onChange)="applyVolValue(volNode.target, 'label', $event)"> + (change)="applyVolValue(volNode.target, 'label', $event.target.value)">
- - @@ -166,23 +195,41 @@
- + +
+ + + + + +
+
+ Duplicate Barcode +
-
@@ -191,7 +238,7 @@
- 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 { - // 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 { + + 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; + } + } } } 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 df54f573a7..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 @@ -1,11 +1,30 @@ +
+
+ +
+
+ - +
+ +
+
+
+ + +
+
+
+ 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 8a5999e41b..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 @@ -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 { + + 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 { - 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 { + + 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 { + + 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 { + + 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 { + + 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 { 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(); + }); + } } 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 aa07966725..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,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; } - */ } diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts index c44a7f7a0b..98c2eb6d4b 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts @@ -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]); diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts index 6b42543382..8044b5af4c 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts @@ -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'); }); }); diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js index 491b6ca10b..0d57d86625 100644 --- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js +++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js @@ -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!');