1 import {Component, OnInit, ViewChild, Input, Renderer2, Output, EventEmitter} from '@angular/core';
2 import {tap} from 'rxjs/operators';
3 import {IdlService, IdlObject} from '@eg/core/idl.service';
4 import {OrgService} from '@eg/core/org.service';
5 import {AuthService} from '@eg/core/auth.service';
6 import {NetService} from '@eg/core/net.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {VolCopyContext, HoldingsTreeNode} from './volcopy';
9 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
10 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
11 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
12 import {VolCopyService} from './volcopy.service';
15 selector: 'eg-vol-edit',
16 templateUrl: 'vol-edit.component.html',
17 styleUrls: ['vol-edit.component.css']
21 export class VolEditComponent implements OnInit {
23 @Input() context: VolCopyContext;
25 // There are 10 columns in the editor form. Set the flex values
26 // here so they don't have to be hard-coded and repeated in the
27 // markup. Changing a flex value here will propagate to all
28 // rows in the form. Column numbers are 1-based.
29 flexSettings: {[column: number]: number} = {
30 1: 2, 2: 1, 3: 2, 4: 1, 5: 2, 6: 1, 7: 1, 8: 2, 9: 1, 10: 1, 11: 1};
32 // Since visibility of some columns is configurable, we need a
33 // map of configured column name to column index.
42 // If a column is specified as the expand field, its flex value
43 // will magically grow.
46 batchVolClass: ComboboxEntry;
47 batchVolPrefix: ComboboxEntry;
48 batchVolSuffix: ComboboxEntry;
49 batchVolLabel: ComboboxEntry;
51 autoBarcodeInProgress = false;
53 deleteVolCount: number = null;
54 deleteCopyCount: number = null;
56 // When adding multiple vols via add-many popover.
57 addVolCount: number = null;
59 // When adding multiple copies via add-many popover.
60 addCopyCount: number = null;
62 recordVolLabels: string[] = [];
64 @ViewChild('confirmDelVol', {static: false})
65 confirmDelVol: ConfirmDialogComponent;
67 @ViewChild('confirmDelCopy', {static: false})
68 confirmDelCopy: ConfirmDialogComponent;
70 // Emitted when the save-ability of this form changes.
71 @Output() canSaveChange: EventEmitter<boolean> = new EventEmitter<boolean>();
74 private renderer: Renderer2,
75 private idl: IdlService,
76 private org: OrgService,
77 private pcrud: PcrudService,
78 private net: NetService,
79 private auth: AuthService,
80 private holdings: HoldingsService,
81 public volcopy: VolCopyService
86 this.deleteVolCount = null;
87 this.deleteCopyCount = null;
89 this.volcopy.genBarcodesRequested.subscribe(() => this.generateBarcodes());
91 this.volcopy.fetchRecordVolLabels(this.context.recordId)
92 .then(labels => this.recordVolLabels = labels)
93 .then(_ => this.volcopy.fetchBibParts(this.context.getRecordIds()))
94 .then(_ => this.addStubCopies())
95 // It's possible the loaded data is not strictly allowed,
96 // e.g. empty string call number labels
97 .then(_ => this.emitSaveChange(true));
100 copyStatLabel(copy: IdlObject): string {
102 const statId = copy.status();
103 if (statId in this.volcopy.copyStatuses) {
104 return this.volcopy.copyStatuses[statId].name();
110 recordHasParts(bibId: number): boolean {
111 return this.volcopy.bibParts[bibId] &&
112 this.volcopy.bibParts[bibId].length > 0;
115 // Column width (flex:x) for column by column number.
116 flexAt(column: number): number {
117 if (!this.displayColumn(this.flexColMap[column])) {
118 // Hidden columsn are still present, but they do not
119 // flex and the contain no data to display
122 let value = this.flexSettings[column];
123 if (this.expand === column) { value = value * 3; }
127 addVol(org: IdlObject) {
128 if (!org) { return; }
129 const orgNode = this.context.findOrCreateOrgNode(org.id());
130 this.createVols(orgNode, 1);
131 this.context.sortHoldings();
134 // This only removes copies that were created during the
135 // current editing session and have not yet been saved in the DB.
136 deleteCopies(volNode: HoldingsTreeNode, count: number) {
137 for (let i = 0; i < count; i++) {
138 const copyNode = volNode.children[volNode.children.length - 1];
139 if (copyNode && copyNode.target.isnew()) {
140 volNode.children.pop();
147 createCopies(volNode: HoldingsTreeNode, count: number) {
149 for (let i = 0; i < count; i++) {
151 // Our context assumes copies are fleshed with volumes
152 const vol = volNode.target;
153 const copy = this.volcopy.createStubCopy(vol);
154 copy.call_number(vol);
155 this.context.findOrCreateCopyNode(copy);
159 this.volcopy.setCopyStatus(copies);
162 createCopiesFromPopover(volNode: HoldingsTreeNode, popover: any) {
163 this.createCopies(volNode, this.addCopyCount);
165 this.addCopyCount = null;
168 createVolsFromPopover(orgNode: HoldingsTreeNode, popover: any) {
169 this.createVols(orgNode, this.addVolCount);
171 this.addVolCount = null;
174 createVols(orgNode: HoldingsTreeNode, count: number) {
177 for (let i = 0; i < count; i++) {
179 // This will vivify the volNode if needed.
180 const vol = this.volcopy.createStubVol(
181 this.context.recordId, orgNode.target.id());
185 // Our context assumes copies are fleshed with volumes
186 const copy = this.volcopy.createStubCopy(vol);
187 copy.call_number(vol);
189 this.context.findOrCreateCopyNode(copy);
192 this.volcopy.setCopyStatus(copies);
193 this.volcopy.setVolClassLabels(vols);
196 // This only removes vols that were created during the
197 // current editing session and have not yet been saved in the DB.
198 deleteVols(orgNode: HoldingsTreeNode, count: number) {
199 for (let i = 0; i < count; i++) {
200 const volNode = orgNode.children[orgNode.children.length - 1];
201 if (volNode && volNode.target.isnew()) {
202 orgNode.children.pop();
209 // When editing existing vols, be sure each has at least one copy.
210 addStubCopies(volNode?: HoldingsTreeNode) {
211 const nodes = volNode ? [volNode] : this.context.volNodes();
214 nodes.forEach(vNode => {
215 if (vNode.children.length === 0) {
216 const vol = vNode.target;
217 const copy = this.volcopy.createStubCopy(vol);
218 copy.call_number(vol);
220 this.context.findOrCreateCopyNode(copy);
224 this.volcopy.setCopyStatus(copies);
227 applyVolValue(vol: IdlObject, key: string, value: any) {
229 if (value === null && (key === 'prefix' || key === 'suffix')) {
230 // -1 is the empty prefix/suffix value.
234 if (vol[key]() !== value) {
239 this.emitSaveChange();
242 applyCopyValue(copy: IdlObject, key: string, value: any) {
243 if (copy[key]() !== value) {
245 copy.ischanged(true);
249 copyPartChanged(copyNode: HoldingsTreeNode, entry: ComboboxEntry) {
250 const copy = copyNode.target;
251 const part = copyNode.target.parts()[0];
256 if (entry.freetext) {
257 newPart = this.idl.create('bmp');
259 newPart.record(copy.call_number().record());
260 newPart.label(entry.label);
265 this.volcopy.bibParts[copy.call_number().record()]
266 .filter(p => p.id() === entry.id)[0];
268 // Nothing to change?
269 if (part && part.id() === newPart.id()) { return; }
272 copy.parts([newPart]);
273 copy.ischanged(true);
275 } else if (part) { // Part map no longer needed.
278 copy.ischanged(true);
283 this.context.volNodes().forEach(volNode => {
284 const vol = volNode.target;
285 if (this.batchVolClass) {
286 this.applyVolValue(vol, 'label_class', this.batchVolClass.id);
288 if (this.batchVolPrefix) {
289 this.applyVolValue(vol, 'prefix', this.batchVolPrefix.id);
291 if (this.batchVolSuffix) {
292 this.applyVolValue(vol, 'suffix', this.batchVolSuffix.id);
294 if (this.batchVolLabel) {
295 // Use label; could be freetext.
296 this.applyVolValue(vol, 'label', this.batchVolLabel.label);
301 // Focus and select the next editable barcode.
302 selectNextBarcode(id: number, previous?: boolean) {
304 let nextId: number = null;
305 let firstId: number = null;
307 let copies = this.context.copyList();
308 if (previous) { copies = copies.reverse(); }
310 // Find the ID of the next item. If this is the last item,
311 // loop back to the first item.
312 copies.forEach(copy => {
313 if (nextId !== null) { return; }
315 // In case we have to loop back to the first copy.
316 if (firstId === null && this.barcodeCanChange(copy)) {
321 if (nextId === null && this.barcodeCanChange(copy)) {
324 } else if (copy.id() === id) {
329 this.renderer.selectRootElement(
330 '#barcode-input-' + (nextId || firstId)).select();
333 barcodeCanChange(copy: IdlObject): boolean {
334 return !this.volcopy.copyStatIsMagic(copy.status());
338 this.autoBarcodeInProgress = true;
340 // Autogen only replaces barcodes for items which are in
342 const copies = this.context.copyList()
343 .filter((copy, idx) => {
344 // During autogen we do not replace the first item,
345 // so it's status is not relevant.
346 return idx === 0 || this.barcodeCanChange(copy);
349 if (copies.length > 1) { // seed barcode will always be present
350 this.proceedWithAutogen(copies)
351 .then(_ => this.autoBarcodeInProgress = false);
355 proceedWithAutogen(copyList: IdlObject[]): Promise<any> {
357 const seedBarcode: string = copyList[0].barcode();
358 copyList.shift(); // Avoid replacing the seed barcode
360 const count = copyList.length;
362 return this.net.request('open-ils.cat',
363 'open-ils.cat.item.barcode.autogen',
364 this.auth.token(), seedBarcode, count, {
365 checkdigit: this.volcopy.defaults.values.use_checkdigit,
368 ).pipe(tap(barcodes => {
370 copyList.forEach(copy => {
371 if (copy.barcode() !== barcodes[0]) {
372 copy.barcode(barcodes[0]);
373 copy.ischanged(true);
381 barcodeChanged(copy: IdlObject, barcode: string) {
384 // Scrub leading/trailing spaces from barcodes
385 barcode = barcode.trim();
386 copy.barcode(barcode);
389 copy.ischanged(true);
390 copy._dupe_barcode = false;
393 this.emitSaveChange();
397 if (!this.autoBarcodeInProgress) {
398 // Manual barcode entry requires dupe check
400 copy._dupe_barcode = false;
401 this.pcrud.search('acp', {
404 id: {'!=': copy.id()}
407 if (resp) { copy._dupe_barcode = true; }
410 () => this.emitSaveChange()
415 deleteCopy(copyNode: HoldingsTreeNode) {
417 if (copyNode.target.isnew()) {
418 // Confirmation not required when deleting brand new copies.
419 this.deleteOneCopy(copyNode);
423 this.deleteCopyCount = 1;
424 this.confirmDelCopy.open().toPromise().then(confirmed => {
425 if (confirmed) { this.deleteOneCopy(copyNode); }
429 deleteOneCopy(copyNode: HoldingsTreeNode) {
430 const targetCopy = copyNode.target;
432 const orgNodes = this.context.orgNodes();
433 for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
434 const orgNode = orgNodes[orgIdx];
436 for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
437 const volNode = orgNode.children[volIdx];
439 for (let copyIdx = 0; copyIdx < volNode.children.length; copyIdx++) {
440 const copy = volNode.children[copyIdx].target;
442 if (copy.id() === targetCopy.id()) {
443 volNode.children.splice(copyIdx, 1);
445 copy.isdeleted(true);
446 this.context.copiesToDelete.push(copy);
449 if (volNode.children.length === 0) {
450 // When removing the last copy, add a stub copy.
451 this.addStubCopies();
462 deleteVol(volNode: HoldingsTreeNode) {
464 if (volNode.target.isnew()) {
465 // Confirmation not required when deleting brand new vols.
466 this.deleteOneVol(volNode);
470 this.deleteVolCount = 1;
471 this.deleteCopyCount = volNode.children.length;
473 this.confirmDelVol.open().toPromise().then(confirmed => {
474 if (confirmed) { this.deleteOneVol(volNode); }
478 deleteOneVol(volNode: HoldingsTreeNode) {
480 let deleteVolIdx = null;
481 const targetVol = volNode.target;
483 // FOR loops allow for early exit
484 const orgNodes = this.context.orgNodes();
485 for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
486 const orgNode = orgNodes[orgIdx];
488 for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
489 const vol = orgNode.children[volIdx].target;
491 if (vol.id() === targetVol.id()) {
492 deleteVolIdx = volIdx;
495 // New volumes, which can only have new copies
496 // may simply be removed from the holdings
497 // tree to delete them.
501 // Mark volume and attached copies as deleted
502 // and track for later deletion.
503 targetVol.isdeleted(true);
504 this.context.volsToDelete.push(targetVol);
506 // When deleting vols, no need to delete the linked
507 // copies. They'll be force deleted via the API.
510 if (deleteVolIdx !== null) { break; }
513 if (deleteVolIdx !== null) {
514 orgNode.children.splice(deleteVolIdx, 1);
520 editVolOwner(volNode: HoldingsTreeNode, org: IdlObject) {
521 if (!org) { return; }
523 const orgId = org.id();
524 const vol = volNode.target;
526 vol.owning_lib(orgId);
529 // Move the vol node away from its previous org node and append
530 // it to the children list of the target node.
531 let targetOrgNode: HoldingsTreeNode;
532 this.context.orgNodes().forEach(orgNode => {
534 if (orgNode.target.id() === orgId) {
535 targetOrgNode = orgNode;
539 orgNode.children.forEach((vNode, volIdx) => {
540 if (vol.id() === vNode.target.id()) {
541 orgNode.children.splice(volIdx, 1);
546 if (!targetOrgNode) {
547 targetOrgNode = this.context.findOrCreateOrgNode(orgId);
550 targetOrgNode.children.push(volNode);
552 // If configured to do so, also update the circ_lib for any
553 // copies linked to this call number in this edit session.
554 if (this.volcopy.defaults.values.circ_lib_mod_with_owning_lib) {
555 volNode.children.forEach(copyNode => {
556 const copy = copyNode.target;
557 if (copy.circ_lib() !== orgId) {
558 copy.circ_lib(orgId);
559 copy.ischanged(true);
564 this.emitSaveChange();
568 displayColumn(field: string): boolean {
569 return this.volcopy.defaults.hidden[field] !== true;
574 const copies = this.context.copyList();
576 const badCopies = copies.filter(copy => {
577 return copy._dupe_barcode || (!copy.isnew() && !copy.barcode());
580 if (badCopies) { return false; }
582 const badVols = this.context.volNodes().filter(volNode => {
583 const vol = volNode.target;
585 vol.prefix() && vol.label() && vol.suffix && vol.label_class()
592 // Called any time a change occurs that could affect the
593 // save-ability of the form.
594 emitSaveChange(initialLoad?: boolean) {
595 const saveable = this.canSave();
597 // Avoid emitting a save change event when this was called
598 // during page load and the resulting data is saveable.
599 if (initialLoad && saveable) { return; }
602 this.canSaveChange.emit(saveable);
606 // Given a DOM ID, focus the element after a 0 timeout.
607 focusElement(domId: string) {
609 const node = document.getElementById(domId);
610 if (node) { node.focus(); }
615 toggleBatchVisibility() {
616 this.volcopy.defaults.visible.batch_actions =
617 !this.volcopy.defaults.visible.batch_actions;
618 this.volcopy.saveDefaults();