1 import {Component, OnInit, AfterViewInit, ViewChild, Input, Renderer2, Output, EventEmitter} from '@angular/core';
2 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
3 import {tap} from 'rxjs/operators';
4 import {IdlService, IdlObject} from '@eg/core/idl.service';
5 import {OrgService} from '@eg/core/org.service';
6 import {AuthService} from '@eg/core/auth.service';
7 import {NetService} from '@eg/core/net.service';
8 import {PcrudService} from '@eg/core/pcrud.service';
9 import {VolCopyContext, HoldingsTreeNode} from './volcopy';
10 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
11 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
12 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
13 import {VolCopyService} from './volcopy.service';
16 selector: 'eg-vol-edit',
17 templateUrl: 'vol-edit.component.html',
18 styleUrls: ['vol-edit.component.css']
22 export class VolEditComponent implements OnInit {
24 @Input() context: VolCopyContext;
26 // There are 10 columns in the editor form. Set the flex values
27 // here so they don't have to be hard-coded and repeated in the
28 // markup. Changing a flex value here will propagate to all
29 // rows in the form. Column numbers are 1-based.
30 flexSettings: {[column: number]: number} = {
31 1: 2, 2: 1, 3: 2, 4: 1, 5: 2, 6: 1, 7: 1, 8: 2, 9: 1, 10: 1, 11: 1};
33 // Since visibility of some columns is configurable, we need a
34 // map of configured column name to column index.
43 // If a column is specified as the expand field, its flex value
44 // will magically grow.
47 batchVolClass: ComboboxEntry;
48 batchVolPrefix: ComboboxEntry;
49 batchVolSuffix: ComboboxEntry;
50 batchVolLabel: ComboboxEntry;
52 autoBarcodeInProgress = false;
54 deleteVolCount: number = null;
55 deleteCopyCount: number = null;
57 // When adding multiple vols via add-many popover.
58 addVolCount: number = null;
60 // When adding multiple copies via add-many popover.
61 addCopyCount: number = null;
63 recordVolLabels: string[] = [];
65 @ViewChild('confirmDelVol', {static: false})
66 confirmDelVol: ConfirmDialogComponent;
68 @ViewChild('confirmDelCopy', {static: false})
69 confirmDelCopy: ConfirmDialogComponent;
71 // Emitted when the save-ability of this form changes.
72 @Output() canSaveChange: EventEmitter<boolean> = new EventEmitter<boolean>();
75 private renderer: Renderer2,
76 private idl: IdlService,
77 private org: OrgService,
78 private pcrud: PcrudService,
79 private net: NetService,
80 private auth: AuthService,
81 private holdings: HoldingsService,
82 public volcopy: VolCopyService
87 this.deleteVolCount = null;
88 this.deleteCopyCount = null;
90 this.volcopy.genBarcodesRequested.subscribe(() => this.generateBarcodes());
92 this.volcopy.fetchRecordVolLabels(this.context.recordId)
93 .then(labels => this.recordVolLabels = labels)
94 .then(_ => this.volcopy.fetchBibParts(this.context.getRecordIds()))
95 .then(_ => this.addStubCopies())
96 // It's possible the loaded data is not strictly allowed,
97 // e.g. empty string call number labels
98 .then(_ => this.emitSaveChange(true));
101 copyStatLabel(copy: IdlObject): string {
103 const statId = copy.status();
104 if (statId in this.volcopy.copyStatuses) {
105 return this.volcopy.copyStatuses[statId].name();
111 recordHasParts(bibId: number): boolean {
112 return this.volcopy.bibParts[bibId] &&
113 this.volcopy.bibParts[bibId].length > 0;
116 // Column width (flex:x) for column by column number.
117 flexAt(column: number): number {
118 if (!this.displayColumn(this.flexColMap[column])) {
119 // Hidden columsn are still present, but they do not
120 // flex and the contain no data to display
123 let value = this.flexSettings[column];
124 if (this.expand === column) { value = value * 3; }
128 addVol(org: IdlObject) {
129 if (!org) { return; }
130 const orgNode = this.context.findOrCreateOrgNode(org.id());
131 this.createVols(orgNode, 1);
132 this.context.sortHoldings();
135 // This only removes copies that were created during the
136 // current editing session and have not yet been saved in the DB.
137 deleteCopies(volNode: HoldingsTreeNode, count: number) {
138 for (let i = 0; i < count; i++) {
139 const copyNode = volNode.children[volNode.children.length - 1];
140 if (copyNode && copyNode.target.isnew()) {
141 volNode.children.pop();
148 createCopies(volNode: HoldingsTreeNode, count: number) {
150 for (let i = 0; i < count; i++) {
152 // Our context assumes copies are fleshed with volumes
153 const vol = volNode.target;
154 const copy = this.volcopy.createStubCopy(vol);
155 copy.call_number(vol);
156 this.context.findOrCreateCopyNode(copy);
160 this.volcopy.setCopyStatus(copies);
163 createCopiesFromPopover(volNode: HoldingsTreeNode, popover: any) {
164 this.createCopies(volNode, this.addCopyCount);
166 this.addCopyCount = null;
169 createVolsFromPopover(orgNode: HoldingsTreeNode, popover: any) {
170 this.createVols(orgNode, this.addVolCount);
172 this.addVolCount = null;
175 createVols(orgNode: HoldingsTreeNode, count: number) {
178 for (let i = 0; i < count; i++) {
180 // This will vivify the volNode if needed.
181 const vol = this.volcopy.createStubVol(
182 this.context.recordId, orgNode.target.id());
186 // Our context assumes copies are fleshed with volumes
187 const copy = this.volcopy.createStubCopy(vol);
188 copy.call_number(vol);
190 this.context.findOrCreateCopyNode(copy);
193 this.volcopy.setCopyStatus(copies);
194 this.volcopy.setVolClassLabels(vols);
197 // This only removes vols that were created during the
198 // current editing session and have not yet been saved in the DB.
199 deleteVols(orgNode: HoldingsTreeNode, count: number) {
200 for (let i = 0; i < count; i++) {
201 const volNode = orgNode.children[orgNode.children.length - 1];
202 if (volNode && volNode.target.isnew()) {
203 orgNode.children.pop();
210 // When editing existing vols, be sure each has at least one copy.
211 addStubCopies(volNode?: HoldingsTreeNode) {
212 const nodes = volNode ? [volNode] : this.context.volNodes();
215 nodes.forEach(vNode => {
216 if (vNode.children.length === 0) {
217 const vol = vNode.target;
218 const copy = this.volcopy.createStubCopy(vol);
219 copy.call_number(vol);
221 this.context.findOrCreateCopyNode(copy);
225 this.volcopy.setCopyStatus(copies);
228 applyVolValue(vol: IdlObject, key: string, value: any) {
230 if (value === null && (key === 'prefix' || key === 'suffix')) {
231 // -1 is the empty prefix/suffix value.
235 if (vol[key]() !== value) {
240 this.emitSaveChange();
243 applyCopyValue(copy: IdlObject, key: string, value: any) {
244 if (copy[key]() !== value) {
246 copy.ischanged(true);
250 copyPartChanged(copyNode: HoldingsTreeNode, entry: ComboboxEntry) {
251 const copy = copyNode.target;
252 const part = copyNode.target.parts()[0];
257 if (entry.freetext) {
258 newPart = this.idl.create('bmp');
260 newPart.record(copy.call_number().record());
261 newPart.label(entry.label);
266 this.volcopy.bibParts[copy.call_number().record()]
267 .filter(p => p.id() === entry.id)[0];
269 // Nothing to change?
270 if (part && part.id() === newPart.id()) { return; }
273 copy.parts([newPart]);
274 copy.ischanged(true);
276 } else if (part) { // Part map no longer needed.
279 copy.ischanged(true);
284 this.context.volNodes().forEach(volNode => {
285 const vol = volNode.target;
286 if (this.batchVolClass) {
287 this.applyVolValue(vol, 'label_class', this.batchVolClass.id);
289 if (this.batchVolPrefix) {
290 this.applyVolValue(vol, 'prefix', this.batchVolPrefix.id);
292 if (this.batchVolSuffix) {
293 this.applyVolValue(vol, 'suffix', this.batchVolSuffix.id);
295 if (this.batchVolLabel) {
296 // Use label; could be freetext.
297 this.applyVolValue(vol, 'label', this.batchVolLabel.label);
302 // Focus and select the next editable barcode.
303 selectNextBarcode(id: number, previous?: boolean) {
305 let nextId: number = null;
306 let firstId: number = null;
308 let copies = this.context.copyList();
309 if (previous) { copies = copies.reverse(); }
311 // Find the ID of the next item. If this is the last item,
312 // loop back to the first item.
313 copies.forEach(copy => {
314 if (nextId !== null) { return; }
316 // In case we have to loop back to the first copy.
317 if (firstId === null && this.barcodeCanChange(copy)) {
322 if (nextId === null && this.barcodeCanChange(copy)) {
325 } else if (copy.id() === id) {
330 this.renderer.selectRootElement(
331 '#barcode-input-' + (nextId || firstId)).select();
334 barcodeCanChange(copy: IdlObject): boolean {
335 return !this.volcopy.copyStatIsMagic(copy.status());
339 this.autoBarcodeInProgress = true;
341 // Autogen only replaces barcodes for items which are in
343 const copies = this.context.copyList()
344 .filter((copy, idx) => {
345 // During autogen we do not replace the first item,
346 // so it's status is not relevant.
347 return idx === 0 || this.barcodeCanChange(copy);
350 if (copies.length > 1) { // seed barcode will always be present
351 this.proceedWithAutogen(copies)
352 .then(_ => this.autoBarcodeInProgress = false);
356 proceedWithAutogen(copyList: IdlObject[]): Promise<any> {
358 const seedBarcode: string = copyList[0].barcode();
359 copyList.shift(); // Avoid replacing the seed barcode
361 const count = copyList.length;
363 return this.net.request('open-ils.cat',
364 'open-ils.cat.item.barcode.autogen',
365 this.auth.token(), seedBarcode, count, {
366 checkdigit: this.volcopy.defaults.values.use_checkdigit,
369 ).pipe(tap(barcodes => {
371 copyList.forEach(copy => {
372 if (copy.barcode() !== barcodes[0]) {
373 copy.barcode(barcodes[0]);
374 copy.ischanged(true);
382 barcodeChanged(copy: IdlObject, barcode: string) {
385 // Scrub leading/trailing spaces from barcodes
386 barcode = barcode.trim();
387 copy.barcode(barcode);
390 copy.ischanged(true);
391 copy._dupe_barcode = false;
394 this.emitSaveChange();
398 if (!this.autoBarcodeInProgress) {
399 // Manual barcode entry requires dupe check
401 copy._dupe_barcode = false;
402 this.pcrud.search('acp', {
405 id: {'!=': copy.id()}
408 if (resp) { copy._dupe_barcode = true; }
411 () => this.emitSaveChange()
416 deleteCopy(copyNode: HoldingsTreeNode) {
418 if (copyNode.target.isnew()) {
419 // Confirmation not required when deleting brand new copies.
420 this.deleteOneCopy(copyNode);
424 this.deleteCopyCount = 1;
425 this.confirmDelCopy.open().toPromise().then(confirmed => {
426 if (confirmed) { this.deleteOneCopy(copyNode); }
430 deleteOneCopy(copyNode: HoldingsTreeNode) {
431 const targetCopy = copyNode.target;
433 const orgNodes = this.context.orgNodes();
434 for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
435 const orgNode = orgNodes[orgIdx];
437 for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
438 const volNode = orgNode.children[volIdx];
440 for (let copyIdx = 0; copyIdx < volNode.children.length; copyIdx++) {
441 const copy = volNode.children[copyIdx].target;
443 if (copy.id() === targetCopy.id()) {
444 volNode.children.splice(copyIdx, 1);
446 copy.isdeleted(true);
447 this.context.copiesToDelete.push(copy);
450 if (volNode.children.length === 0) {
451 // When removing the last copy, add a stub copy.
452 this.addStubCopies();
463 deleteVol(volNode: HoldingsTreeNode) {
465 if (volNode.target.isnew()) {
466 // Confirmation not required when deleting brand new vols.
467 this.deleteOneVol(volNode);
471 this.deleteVolCount = 1;
472 this.deleteCopyCount = volNode.children.length;
474 this.confirmDelVol.open().toPromise().then(confirmed => {
475 if (confirmed) { this.deleteOneVol(volNode); }
479 deleteOneVol(volNode: HoldingsTreeNode) {
481 let deleteVolIdx = null;
482 const targetVol = volNode.target;
484 // FOR loops allow for early exit
485 const orgNodes = this.context.orgNodes();
486 for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
487 const orgNode = orgNodes[orgIdx];
489 for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
490 const vol = orgNode.children[volIdx].target;
492 if (vol.id() === targetVol.id()) {
493 deleteVolIdx = volIdx;
496 // New volumes, which can only have new copies
497 // may simply be removed from the holdings
498 // tree to delete them.
502 // Mark volume and attached copies as deleted
503 // and track for later deletion.
504 targetVol.isdeleted(true);
505 this.context.volsToDelete.push(targetVol);
507 // When deleting vols, no need to delete the linked
508 // copies. They'll be force deleted via the API.
511 if (deleteVolIdx !== null) { break; }
514 if (deleteVolIdx !== null) {
515 orgNode.children.splice(deleteVolIdx, 1);
521 displayColumn(field: string): boolean {
522 return this.volcopy.defaults.hidden[field] !== true;
527 const copies = this.context.copyList();
529 const badCopies = copies.filter(copy => {
530 return copy._dupe_barcode || (!copy.isnew() && !copy.barcode());
533 if (badCopies) { return false; }
535 const badVols = this.context.volNodes().filter(volNode => {
536 const vol = volNode.target;
538 vol.prefix() && vol.label() && vol.suffix && vol.label_class()
545 // Called any time a change occurs that could affect the
546 // save-ability of the form.
547 emitSaveChange(initialLoad?: boolean) {
548 const saveable = this.canSave();
550 // Avoid emitting a save change event when this was called
551 // during page load and the resulting data is saveable.
552 if (initialLoad && saveable) { return; }
555 this.canSaveChange.emit(saveable);
559 // Given a DOM ID, focus the element after a 0 timeout.
560 focusElement(domId: string) {
562 const node = document.getElementById(domId);
563 if (node) { node.focus(); }
568 toggleBatchVisibility() {
569 this.volcopy.defaults.visible.batch_actions =
570 !this.volcopy.defaults.visible.batch_actions;
571 this.volcopy.saveDefaults();