}
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) {
<div class="d-flex">
<input type="text"
class="form-control"
- [ngClass]="{'text-success font-italic font-weight-bold': selected && selected.freetext}"
+ [ngClass]="{
+ 'text-success font-italic font-weight-bold': selected && selected.freetext,
+ 'form-control-sm': smallFormControl
+ }"
[placeholder]="placeholder"
[name]="name"
[disabled]="isDisabled"
@Input() inputSize: number = null;
+ // If true, applies form-control-sm CSS
+ @Input() smallFormControl = false;
+
// Add a 'required' attribute to the input
isRequired: boolean;
@Input() set required(r: boolean) {
}, {
path: 'authority',
loadChildren: '@eg/staff/cat/authority/authority.module#AuthorityModule'
+ }, {
+ path: 'volcopy',
+ loadChildren: '@eg/staff/cat/volcopy/volcopy.module#VolCopyModule'
}
];
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {VolCopyComponent} from './volcopy.component';
+
+const routes: Routes = [{
+ path: 'edit/item/:copy_id',
+ component: VolCopyComponent
+ }, {
+ path: 'edit/callnumber/:vol_id',
+ component: VolCopyComponent
+ }, {
+ path: 'edit/record/:record_id',
+ component: VolCopyComponent
+ }, {
+ path: 'edit/session/:session',
+ component: VolCopyComponent
+ /*
+ }, {
+ path: 'templates'
+ component: VolCopyComponent
+ }, {
+ path: 'configure'
+ component: VolCopyComponent
+ */
+}];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+ providers: []
+})
+
+export class VolCopyRoutingModule {}
+
--- /dev/null
+
+input[type="number"] {
+ /* visually accomodates numbers in the hundreds */
+ width: 4.5em;
+}
+
+.vol-row {
+ background-color: rgba(0,0,0,.03);
+ border-top: 1px solid rgba(0,0,0,.125);
+ border-bottom: 1px solid rgba(0,0,0,.125);
+}
--- /dev/null
+
+<div class="row d-flex vol-row border border-info mb-2">
+ <div class="p-1" [ngStyle]="{flex: flexAt(1)}">
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(2)}">
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(3)}">
+ <label class="font-weight-bold" i18n>Classification</label>
+ <div>
+ <eg-combobox [smallFormControl]="true" [(ngModel)]="batchVolClass">
+ <eg-combobox-entry *ngFor="let cls of volClasses"
+ [entryId]="cls.id()" [entryLabel]="cls.name()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(4)}">
+ <label class="font-weight-bold" i18n>Prefix</label>
+ <div>
+ <eg-combobox [smallFormControl]="true" [(ngModel)]="batchVolPrefix">
+ <eg-combobox-entry *ngFor="let pfx of volPrefixes"
+ [entryId]="pfx.id()" [entryLabel]="pfx.label()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(5)}">
+ <label class="font-weight-bold" i18n>Call Number Label</label>
+ <div>
+ <eg-combobox [smallFormControl]="true" [(ngModel)]="batchVolLabel">
+ <eg-combobox-entry *ngFor="let label of recordVolLabels" [entryId]="label">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(6)}">
+ <label class="font-weight-bold" i18n>Suffix</label>
+ <div>
+ <eg-combobox [smallFormControl]="true" [(ngModel)]="batchVolSuffix">
+ <eg-combobox-entry *ngFor="let sfx of volSuffixes"
+ [entryId]="sfx.id()" [entryLabel]="sfx.label()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(7)}">
+ <label class="font-weight-bold" i18n>Batch</label>
+ <div>
+ <button class="btn btn-sm btn-outline-dark label-with-material-icon"
+ (click)="batchVolApply()">
+ <span i18n>Apply</span>
+ <span class="material-icons">arrow_downward</span>
+ </button>
+ </div>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(8)}">
+ <label class="font-weight-bold" i18n>Generate Barcodes</label>
+ <button class="btn btn-sm btn-outline-dark label-with-material-icon"
+ (click)="generateBarcodes()">
+ <span i18n>Generate</span>
+ <span class="material-icons">arrow_downward</span>
+ </button>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(9)}"></div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(10)}"></div>
+</div>
+
+
+<div class="row d-flex mt-2 mb-2">
+ <div class="p-1" [ngStyle]="{flex: flexAt(1)}">
+ <label class="font-weight-bold" i18n>Owning Library</label>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(2)}">
+ <label class="font-weight-bold" i18n>Call Numbers</label>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(3)}">
+ <label class="font-weight-bold" i18n>Classification</label>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(4)}">
+ <label class="font-weight-bold" i18n>Prefix</label>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(5)}">
+ <label class="font-weight-bold" i18n>Call Number Label</label>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(6)}">
+ <label class="font-weight-bold" i18n>Suffix</label>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(7)}">
+ <label class="font-weight-bold" i18n>Items</label>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(8)}">
+ <label class="font-weight-bold" i18n>Barcode</label>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(9)}">
+ <label class="font-weight-bold" i18n>Item #</label>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(10)}">
+ <label class="font-weight-bold" i18n>Part</label>
+ </div>
+</div>
+
+
+<ng-container *ngFor="let orgNode of context.orgNodes(); let orgIdx = index">
+ <ng-container *ngFor="let volNode of orgNode.children; let volIdx = index">
+ <ng-container *ngFor="let copyNode of volNode.children; let copyIdx = index">
+ <div class="row d-flex mt-1" [ngClass]="{'vol-row': copyIdx == 0}">
+ <div class="p-1" [ngStyle]="{flex: flexAt(1)}">
+ <span *ngIf="copyIdx == 0">{{orgNode.target.shortname()}}</span>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(2)}">
+ <ng-container *ngIf="copyIdx == 0">
+ <input type="number" class="form-control form-control-sm"
+ [required]="true"
+ [ngModel]="orgNode.children.length"
+ (ngModelChange)="volCountChanged(orgNode, $event)"/>
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(3)}">
+ <ng-container *ngIf="copyIdx == 0">
+ <eg-combobox
+ [selectedId]="volNode.target.label_class()"
+ [smallFormControl]="true"
+ [required]="true"
+ (onChange)="applyVolValue(volNode.target, 'label_class', $event ? $event.id : null)">
+ <eg-combobox-entry *ngFor="let cls of volClasses"
+ [entryId]="cls.id()" [entryLabel]="cls.name()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(4)}">
+ <ng-container *ngIf="copyIdx == 0">
+ <eg-combobox
+ [selectedId]="volNode.target.prefix()"
+ [required]="true"
+ [smallFormControl]="true"
+ (onChange)="applyVolValue(volNode.target, 'prefix', $event ? $event.id : null)">
+ <eg-combobox-entry *ngFor="let pfx of volPrefixes"
+ [entryId]="pfx.id()" [entryLabel]="pfx.label()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(5)}">
+ <ng-container *ngIf="copyIdx == 0">
+ <input class="form-control form-control-sm" type="text"
+ spellcheck="false"
+ [required]="true"
+ [ngModel]="volNode.target.label()"
+ (onChange)="applyVolValue(volNode.target, 'label', $event)">
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(6)}">
+ <ng-container *ngIf="copyIdx == 0">
+ <eg-combobox
+ [selectedId]="volNode.target.suffix()"
+ [required]="true"
+ [smallFormControl]="true"
+ (onChange)="applyVolValue(volNode.target, 'suffix', $event ? $event.id : null)">
+ <eg-combobox-entry *ngFor="let sfx of volSuffixes"
+ [entryId]="sfx.id()" [entryLabel]="sfx.label()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(7)}">
+ <ng-container *ngIf="copyIdx == 0">
+ <input type="number" class="form-control form-control-sm"
+ [required]="true"
+ [ngModel]="volNode.children.length"
+ (ngModelChange)="copyCountChanged(volNode, $event)"/>
+ </ng-container>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(8)}">
+ <input type="text" class="form-control form-control-sm"
+ id="barcode-input-{{copyNode.target.id()}}"
+ spellcheck="false"
+ [required]="true"
+ (keyup.enter)="selectNextBarcode(copyNode.target.id())"
+ (keyup.shift.enter)="selectNextBarcode(copyNode.target.id(), true)"
+ [ngModel]="copyNode.target.barcode()"
+ (ngModelChange)="applyCopyValue(copyNode.target, 'barcode', $event)"/>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(9)}">
+ <input type="number" class="form-control form-control-sm"
+ [ngModel]="copyNode.target.copy_number()"
+ (ngModelChange)="applyCopyValue(copyNode.target, 'copy_number', $event)"/>
+ </div>
+ <div class="p-1" [ngStyle]="{flex: flexAt(10)}">
+ <ng-container *ngIf="!recordHasParts(volNode.target.record())">
+ <label i18n>N/A</label>
+ </ng-container>
+ <ng-container *ngIf="recordHasParts(volNode.target.record())">
+ <eg-combobox
+ [disabled]="bibParts[volNode.target.record()].length == 0"
+ [selectedId]="copyNode.target.parts()[0] ? copyNode.target.parts()[0].id() : null"
+ [smallFormControl]="true"
+ (onChange)="copyPartChanged(copyNode, $event)">
+ <eg-combobox-entry *ngFor="let part of bibParts[volNode.target.record()]"
+ [entryId]="part.id()" [entryLabel]="part.label()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-container>
+ </div>
+ </div>
+ </ng-container>
+ </ng-container>
+</ng-container>
--- /dev/null
+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<any> {
+ // 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() {
+ }
+}
+
--- /dev/null
+<eg-staff-banner bannerText="Holdings Editor" i18n-bannerText></eg-staff-banner>
+
+<ng-container *ngIf="!loading">
+
+ <eg-bib-summary *ngIf="recordId" [recordId]="recordId"></eg-bib-summary>
+
+ <div class="mt-3">
+ <eg-vol-edit [context]="context"></eg-vol-edit>
+ </div>
+
+</ng-container>
--- /dev/null
+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<any> {
+ 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<any> {
+ 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<any> {
+ 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<any> {
+ const ids = [].concat(recordIds);
+
+ return this.pcrud.search('acn',
+ {record: ids, deleted: 'f'},
+ {}, {idlist: true, atomic: true}
+ ).toPromise().then(volIds =>this.fetchVols(volIds));
+ }
+}
+
+
+
--- /dev/null
+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 {
+}
--- /dev/null
+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;
+ }
+ */
+}
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<any> {
+ 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 {
* 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;
@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
) {}
});
});
}
+
+ // Returns a sorted list of call number classes
+ fetchCallNumberClasses(): Promise<IdlObject[]> {
+ 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<IdlObject[]> {
+ 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<IdlObject[]> {
+ 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;
+ });
+ }
}