From: Bill Erickson Date: Thu, 4 Jun 2020 16:36:09 +0000 (-0400) Subject: LPXXX Angular Volcopy X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=088882e9d8d671b174f4a4b97cedf458b2ac9d33;p=working%2FEvergreen.git LPXXX Angular Volcopy Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts index c80d0b2683..39126c2c42 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts @@ -361,7 +361,7 @@ export class CatalogService { } iconFormatLabel(code: string): string { - if (this.ccvmMap) { + if (this.ccvmMap && this.ccvmMap.icon_format) { const ccvm = this.ccvmMap.icon_format.filter( format => format.code() === code)[0]; if (ccvm) { diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html index 83df22e20a..c456f80043 100644 --- a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html +++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html @@ -7,7 +7,10 @@
+
+
+
+
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+
+
+ +
+ + + + +
+
+
+ +
+ +
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + +
+
+ {{orgNode.target.shortname()}} +
+
+ + + +
+
+ + + + + + +
+
+ + + + + + +
+
+ + + +
+
+ + + + + + +
+
+ + + +
+
+ +
+
+ +
+
+ + + + + + + + + +
+
+
+
+
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 new file mode 100644 index 0000000000..6b9c33a60d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.ts @@ -0,0 +1,207 @@ +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 {OrgService} from '@eg/core/org.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'; + +@Component({ + selector: 'eg-vol-edit', + templateUrl: 'vol-edit.component.html', + styleUrls: ['vol-edit.component.css'] +}) + + +export class VolEditComponent implements OnInit { + + @Input() context: VolCopyContext; + + // There are 10 columns in the editor form. Set the flex values + // here so they don't have to be hard-coded and repeated in the + // markup. Changing a flex value here will propagate to all + // rows in the form. + flexSettings: {[column: number]: number} = { + 1: 1, 2: 1, 3: 2, 4: 1, 5: 2, 6: 1, 7: 1, 8: 2, 9: 1, 10: 1}; + + volClasses: IdlObject[] = null; + volPrefixes: IdlObject[] = null; + volSuffixes: IdlObject[] = null; + bibParts: {[bibId: number]: IdlObject[]} = {}; + + batchVolClass: ComboboxEntry; + batchVolPrefix: ComboboxEntry; + batchVolSuffix: ComboboxEntry; + batchVolLabel: ComboboxEntry; + + recordVolLabels: string[] = []; + + constructor( + private renderer: Renderer2, + private pcrud: PcrudService, + private net: NetService, + private holdings: HoldingsService + ) {} + + ngOnInit() { + + this.fetchRecordVolLabels() + .then(_ => this.fetchBibParts()); + + // TODO: Filter these to only show org-scoped values + // plus any values otherwise needed for the current + // holdings tree. + + this.holdings.fetchCallNumberClasses().then( + classes => this.volClasses = classes); + + this.holdings.fetchCallNumberPrefixes().then(prefixes => { + this.volPrefixes = prefixes.filter(pfx => pfx.id() !== -1) + }); + + this.holdings.fetchCallNumberSuffixes().then(suffixes => + 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() { + + this.context.orgNodes().forEach(orgNode => { + orgNode.children.forEach(volNode => + this.bibParts[volNode.target.record()] = [] + ); + }); + + this.pcrud.search('bmp', + {record: Object.keys(this.bibParts), deleted: 'f'}) + .subscribe( + part => { + if (!this.bibParts[part.record()]) { + this.bibParts[part.record()] = []; + } + this.bibParts[part.record()].push(part); + }, + err => {}, + () => { + Object.keys(this.bibParts).forEach(bibId => { + this.bibParts[bibId] = this.bibParts[bibId] + .sort((p1, p2) => + p1.label_sortkey() < p2.label_sortkey() ? -1 : 1) + }); + } + ); + } + + recordHasParts(bibId: number): boolean { + return this.bibParts[bibId] && this.bibParts[bibId].length > 0; + } + + flexAt(column: number): number { + return this.flexSettings[column]; + } + + volCountChanged(orgNode: HoldingsTreeNode, count: number) { + console.log('vol set set to ', count); + } + + copyCountChanged(volNode: HoldingsTreeNode, count: number) { + console.log('vol set set to ', count); + } + + applyVolValue(vol: IdlObject, key: string, value: any) { + + if (value === null && (key === 'prefix' || key === 'suffix')) { + // -1 is the empty prefix/suffix value. + value = -1; + } + + if (vol[key]() !== value) { + vol[key](value); + vol.ischanged(true); + } + } + + applyCopyValue(copy: IdlObject, key: string, value: any) { + if (copy[key]() !== value) { + copy[key](value); + copy.ischanged(true); + } + } + + copyPartChanged(copyNode: HoldingsTreeNode, entry: ComboboxEntry) { + // TODO + } + + 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); + } + if (this.batchVolPrefix) { + this.applyVolValue(vol, 'prefix', this.batchVolPrefix.id); + } + if (this.batchVolSuffix) { + this.applyVolValue(vol, 'suffix', this.batchVolSuffix.id); + } + if (this.batchVolLabel) { + this.applyVolValue(vol, 'label', this.batchVolLabel.id); + } + }); + } + + selectNextBarcode(id: number, previous?: boolean) { + let found = false; + let nextId: number = null; + let firstId: number = null; + + let copies = this.context.copyList(); + if (previous) { copies = copies.reverse(); } + + // Find the ID of the next item. If this is the last item, + // loop back to the first item. + copies.forEach(copy => { + if (nextId !== null) { return; } + + // In case we have to loop back to the first copy. + if (firstId === null) { firstId = copy.id(); } + + if (found) { + if (nextId === null) { + nextId = copy.id(); + } + } else if (copy.id() === id) { + found = true; + } + }); + + this.renderer.selectRootElement( + '#barcode-input-' + (nextId || firstId)).select(); + } + + generateBarcodes() { + } +} + 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 new file mode 100644 index 0000000000..df54f573a7 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.html @@ -0,0 +1,11 @@ + + + + + + +
+ +
+ +
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 new file mode 100644 index 0000000000..8a5999e41b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.ts @@ -0,0 +1,126 @@ +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 {OrgService} from '@eg/core/org.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {HoldingsService} from '@eg/staff/share/holdings/holdings.service'; +import {VolCopyContext} from './volcopy'; + +const COPY_FLESH = { + flesh: 1, + flesh_fields: { + acp: ['call_number', 'location', 'parts'] + } +} + +@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; + + constructor( + private router: Router, + private route: ActivatedRoute, + private renderer: Renderer2, + private org: OrgService, + private pcrud: PcrudService, + private holdings: HoldingsService + ) { } + + ngOnInit() { + this.context = new VolCopyContext(); + this.context.org = this.org; // inject; + + this.route.paramMap.subscribe( + (params: ParamMap) => this.negotiateRoute(params)); + } + + 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.load(); + } + + load() { + this.loading = true; + this.context.reset(); + this.fetchHoldings() + .then(_ => this.holdings.fetchCallNumberClasses()) + .then(_ => this.holdings.fetchCallNumberPrefixes()) + .then(_ => this.holdings.fetchCallNumberSuffixes()) + .then(_ => this.context.sortHoldings()) + .then(_ => this.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.copyId) { + return this.fetchCopies(this.copyId); + } else if (this.volId) { + return this.fetchVols(this.volId); + } else if (this.recordId) { + return this.fetchRecords(this.recordId); + } + } + + + fetchCopies(copyIds: number | number[]): Promise { + const ids = [].concat(copyIds); + return this.pcrud.search('acp', {id: ids}, COPY_FLESH) + .pipe(tap(copy => this.context.findOrCreateCopyNode(copy))) + .toPromise(); + } + + // Fetch call numbers and 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 => { + 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. + fetchRecords(recordIds: number | number[]): Promise { + const ids = [].concat(recordIds); + + return this.pcrud.search('acn', + {record: ids, deleted: 'f'}, + {}, {idlist: true, atomic: true} + ).toPromise().then(volIds =>this.fetchVols(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 new file mode 100644 index 0000000000..cdd9e19d16 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.module.ts @@ -0,0 +1,25 @@ +import {NgModule} from '@angular/core'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {CommonWidgetsModule} from '@eg/share/common-widgets.module'; +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'; + +@NgModule({ + declarations: [ + VolCopyComponent, + VolEditComponent + ], + imports: [ + StaffCommonModule, + CommonWidgetsModule, + HoldingsModule, + VolCopyRoutingModule + ], + providers: [ + ] +}) + +export class VolCopyModule { +} 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 new file mode 100644 index 0000000000..aa07966725 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.ts @@ -0,0 +1,157 @@ +import {IdlObject} from '@eg/core/idl.service'; +import {OrgService} from '@eg/core/org.service'; + +export class HoldingsTreeNode { + children: HoldingsTreeNode[]; + nodeType: 'org' | 'vol' | 'copy'; + target: any; + parentNode: HoldingsTreeNode; + constructor() { + this.children = []; + } +} + +class HoldingsTree { + root: HoldingsTreeNode; + constructor() { + this.root = new HoldingsTreeNode(); + } +} + +export class VolCopyContext { + + autoId = -1; + holdings: HoldingsTree = new HoldingsTree(); + org: OrgService; // injected + + reset() { + this.holdings = new HoldingsTree(); + } + + orgNodes(): HoldingsTreeNode[] { + return this.holdings.root.children; + } + + volNodes(): HoldingsTreeNode[] { + let vols = []; + this.orgNodes().forEach(orgNode => + vols = vols.concat(orgNode.children)); + return vols; + } + + copyList(): IdlObject[] { + let copies = []; + this.volNodes().forEach(volNode => { + copies = copies.concat(volNode.children.map(c => c.target)); + }); + return copies; + } + + // 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) + }); + + return Object.keys(idHash).map(id => Number(id)); + } + + // Adds an org unit node; unsorted. + findOrCreateOrgNode(orgId: number): HoldingsTreeNode { + + const existing: HoldingsTreeNode = + this.orgNodes().filter(n => n.target.id() === orgId)[0]; + + if (existing) { return existing; } + + const node: HoldingsTreeNode = new HoldingsTreeNode(); + node.nodeType = 'org'; + node.target = this.org.get(orgId); + node.parentNode = this.holdings.root; + + this.orgNodes().push(node); + + return node; + } + + findOrCreateVolNode(vol: IdlObject): HoldingsTreeNode { + const orgId = vol.owning_lib(); + const orgNode = this.findOrCreateOrgNode(orgId); + + const existing = orgNode.children.filter( + n => n.target.id() === vol.id())[0]; + + if (existing) { return existing; } + + const node: HoldingsTreeNode = new HoldingsTreeNode(); + node.nodeType = 'vol'; + node.target = vol; + node.parentNode = orgNode; + + orgNode.children.push(node); + + return node; + } + + + findOrCreateCopyNode(copy: IdlObject): HoldingsTreeNode { + + const volNode = this.findOrCreateVolNode(copy.call_number()); + + const existing = volNode.children.filter( + c => c.target.id() === copy.id())[0]; + + if (existing) { return existing; } + + const node: HoldingsTreeNode = new HoldingsTreeNode(); + node.nodeType = 'copy'; + node.target = copy; + node.parentNode = volNode; + + volNode.children.push(node); + + return node; + } + + + sortHoldings() { + + this.orgNodes().forEach(orgNode => { + orgNode.children.forEach(volNode => { + + // Sort copys by barcode code + volNode.children = volNode.children.sort((c1, c2) => + c1.target.barcode() < c2.target.barcode() ? -1 : 1); + + }); + + // Sort call numbers by label + orgNode.children = orgNode.children.sort((c1, c2) => + c1.target.label() < c2.target.label() ? -1 : 1); + }); + + // sort org units by shortname + this.holdings.root.children = this.orgNodes().sort((o1, o2) => + o1.target.shortname() < o2.target.shortname() ? -1 : 1); + } + + // Sorted list of holdings tree nodes + /* + flattenHoldings(): HoldingsTreeNode[] { + this.sortHoldings(); + let nodes: HoldingsTreeNode[] = []; + + this.orgNodes().forEach(orgNode => { + nodes.push(orgNode); + orgNode.children.forEach(volNode => { + nodes.push(volNode); + nodes = nodes.concat(volNode.children); + }); + }); + + return nodes; + } + */ +} diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts index 39b8944d3a..4b1a4b830b 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/bib-summary/bib-summary.component.ts @@ -45,30 +45,30 @@ export class BibSummaryComponent implements OnInit { ngOnInit() { - if (this.summary) { - this.summary.getBibCallNumber(); - } else { - if (this.recordId) { - this.loadSummary(); - } - } - this.store.getItem('eg.cat.record.summary.collapse') .then(value => this.expand = !value) - .then(() => this.initDone = true); + .then(_ => this.cat.fetchCcvms()) + .then(_ => { + if (this.summary) { + return this.summary.getBibCallNumber(); + } else { + if (this.recordId) { + return this.loadSummary(); + } + } + }).then(_ => this.initDone = true); } saveExpandState() { this.store.setItem('eg.cat.record.summary.collapse', !this.expand); } - loadSummary(): void { - this.bib.getBibSummary(this.recordId).toPromise() + loadSummary(): Promise { + return this.bib.getBibSummary(this.recordId).toPromise() .then(summary => { - summary.getBibCallNumber(); - this.bib.fleshBibUsers([summary.record]); this.summary = summary; - }); + return summary.getBibCallNumber(); + }).then(_ => this.bib.fleshBibUsers([this.summary.record])); } orgName(orgId: number): string { 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 5c91a68474..6b42543382 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 @@ -2,10 +2,12 @@ * Common code for mananging holdings */ import {Injectable, EventEmitter} from '@angular/core'; +import {IdlObject, IdlService} from '@eg/core/idl.service'; import {NetService} from '@eg/core/net.service'; import {AnonCacheService} from '@eg/share/util/anon-cache.service'; import {AuthService} from '@eg/core/auth.service'; import {EventService} from '@eg/core/event.service'; +import {PcrudService} from '@eg/core/pcrud.service'; interface NewCallNumData { owner?: number; @@ -17,9 +19,14 @@ interface NewCallNumData { @Injectable() export class HoldingsService { + callNumberClasses: IdlObject[]; + callNumberPrefixes: IdlObject[]; + callNumberSuffixes: IdlObject[]; + constructor( private net: NetService, private auth: AuthService, + private pcrud: PcrudService, private evt: EventService, private anonCache: AnonCacheService ) {} @@ -57,5 +64,47 @@ export class HoldingsService { }); }); } + + // Returns a sorted list of call number classes + fetchCallNumberClasses(): Promise { + if (this.callNumberClasses) { + return Promise.resolve(this.callNumberClasses); + } + + return this.pcrud.retrieveAll('acnc', {}, {atomic: true}) + .toPromise().then(classes => { + this.callNumberClasses = classes.sort( + (c1, c2) => c1.name() < c2.name() ? -1 : 1); + return this.callNumberClasses; + }); + } + + // Returns a sorted list of call number prefixes + fetchCallNumberPrefixes(): Promise { + if (this.callNumberPrefixes) { + return Promise.resolve(this.callNumberPrefixes); + } + + return this.pcrud.retrieveAll('acnp', {}, {atomic: true}) + .toPromise().then(prefixes => { + this.callNumberPrefixes = prefixes.sort( + (c1, c2) => c1.label_sortkey() < c2.label_sortkey() ? -1 : 1); + return this.callNumberPrefixes; + }); + } + + // Returns a sorted list of call number suffixes + fetchCallNumberSuffixes(): Promise { + if (this.callNumberSuffixes) { + return Promise.resolve(this.callNumberSuffixes); + } + + return this.pcrud.retrieveAll('acns', {}, {atomic: true}) + .toPromise().then(suffixes => { + this.callNumberSuffixes = suffixes.sort( + (c1, c2) => c1.label_sortkey() < c2.label_sortkey() ? -1 : 1); + return this.callNumberSuffixes; + }); + } }