]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.ts
LP1888723 Improve copy default status lookup
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / cat / volcopy / vol-edit.component.ts
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';
14
15 @Component({
16   selector: 'eg-vol-edit',
17   templateUrl: 'vol-edit.component.html',
18   styleUrls: ['vol-edit.component.css']
19 })
20
21
22 export class VolEditComponent implements OnInit {
23
24     @Input() context: VolCopyContext;
25
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};
32
33     // Since visibility of some columns is configurable, we need a
34     // map of configured column name to column index.
35     flexColMap = {
36         3: 'classification',
37         4: 'prefix',
38         6: 'suffix',
39         9: 'copy_number_vc',
40         10: 'copy_part'
41     };
42
43     // If a column is specified as the expand field, its flex value
44     // will magically grow.
45     expand: number;
46
47     batchVolClass: ComboboxEntry;
48     batchVolPrefix: ComboboxEntry;
49     batchVolSuffix: ComboboxEntry;
50     batchVolLabel: ComboboxEntry;
51
52     autoBarcodeInProgress = false;
53
54     deleteVolCount: number = null;
55     deleteCopyCount: number = null;
56
57     // When adding multiple vols via add-many popover.
58     addVolCount: number = null;
59
60     // When adding multiple copies via add-many popover.
61     addCopyCount: number = null;
62
63     recordVolLabels: string[] = [];
64
65     @ViewChild('confirmDelVol', {static: false})
66         confirmDelVol: ConfirmDialogComponent;
67
68     @ViewChild('confirmDelCopy', {static: false})
69         confirmDelCopy: ConfirmDialogComponent;
70
71     // Emitted when the save-ability of this form changes.
72     @Output() canSaveChange: EventEmitter<boolean> = new EventEmitter<boolean>();
73
74     constructor(
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
83     ) {}
84
85     ngOnInit() {
86
87         this.deleteVolCount = null;
88         this.deleteCopyCount = null;
89
90         this.volcopy.genBarcodesRequested.subscribe(() => this.generateBarcodes());
91
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     }
97
98     copyStatLabel(copy: IdlObject): string {
99         if (copy) {
100             const statId = copy.status();
101             if (statId in this.volcopy.copyStatuses) {
102                 return this.volcopy.copyStatuses[statId].name();
103             }
104         }
105         return '';
106     }
107
108     recordHasParts(bibId: number): boolean {
109         return this.volcopy.bibParts[bibId] &&
110             this.volcopy.bibParts[bibId].length > 0;
111     }
112
113     // Column width (flex:x) for column by column number.
114     flexAt(column: number): number {
115         if (!this.displayColumn(this.flexColMap[column])) {
116             // Hidden columsn are still present, but they do not
117             // flex and the contain no data to display
118             return 0;
119         }
120         let value = this.flexSettings[column];
121         if (this.expand === column) { value = value * 3; }
122         return value;
123     }
124
125     addVol(org: IdlObject) {
126         if (!org) { return; }
127         const orgNode = this.context.findOrCreateOrgNode(org.id());
128         this.createVols(orgNode, 1);
129     }
130
131     // This only removes copies that were created during the
132     // current editing session and have not yet been saved in the DB.
133     deleteCopies(volNode: HoldingsTreeNode, count: number) {
134         for (let i = 0;  i < count; i++) {
135             const copyNode = volNode.children[volNode.children.length - 1];
136             if (copyNode && copyNode.target.isnew()) {
137                 volNode.children.pop();
138             } else {
139                 break;
140             }
141         }
142     }
143
144     createCopies(volNode: HoldingsTreeNode, count: number) {
145         const copies = [];
146         for (let i = 0; i < count; i++) {
147
148             // Our context assumes copies are fleshed with volumes
149             const vol = volNode.target;
150             const copy = this.volcopy.createStubCopy(vol);
151             copy.call_number(vol);
152             this.context.findOrCreateCopyNode(copy);
153             copies.push(copy);
154         }
155
156         this.volcopy.setCopyStatus(copies);
157     }
158
159     createCopiesFromPopover(volNode: HoldingsTreeNode, popover: any) {
160         this.createCopies(volNode, this.addCopyCount);
161         popover.close();
162         this.addCopyCount = null;
163     }
164
165     createVolsFromPopover(orgNode: HoldingsTreeNode, popover: any) {
166         this.createVols(orgNode, this.addVolCount);
167         popover.close();
168         this.addVolCount = null;
169     }
170
171     createVols(orgNode: HoldingsTreeNode, count: number) {
172         const vols = [];
173         const copies = [];
174         for (let i = 0; i < count; i++) {
175
176             // This will vivify the volNode if needed.
177             const vol = this.volcopy.createStubVol(
178                 this.context.recordId, orgNode.target.id());
179
180             vols.push(vol);
181
182             // Our context assumes copies are fleshed with volumes
183             const copy = this.volcopy.createStubCopy(vol);
184             copy.call_number(vol);
185             copies.push(copy);
186             this.context.findOrCreateCopyNode(copy);
187         }
188
189         this.volcopy.setCopyStatus(copies);
190         this.volcopy.setVolClassLabels(vols);
191     }
192
193     // This only removes vols that were created during the
194     // current editing session and have not yet been saved in the DB.
195     deleteVols(orgNode: HoldingsTreeNode, count: number) {
196         for (let i = 0;  i < count; i++) {
197             const volNode = orgNode.children[orgNode.children.length - 1];
198             if (volNode && volNode.target.isnew()) {
199                 orgNode.children.pop();
200             } else {
201                 break;
202             }
203         }
204     }
205
206     // When editing existing vols, be sure each has at least one copy.
207     addStubCopies(volNode?: HoldingsTreeNode) {
208         const nodes = volNode ? [volNode] : this.context.volNodes();
209
210         const copies = [];
211         nodes.forEach(vNode => {
212             if (vNode.children.length === 0) {
213                 const vol = vNode.target;
214                 const copy = this.volcopy.createStubCopy(vol);
215                 copy.call_number(vol);
216                 copies.push(copy);
217                 this.context.findOrCreateCopyNode(copy);
218             }
219         });
220
221         this.volcopy.setCopyStatus(copies);
222     }
223
224     applyVolValue(vol: IdlObject, key: string, value: any) {
225
226         if (value === null && (key === 'prefix' || key === 'suffix')) {
227             // -1 is the empty prefix/suffix value.
228             value = -1;
229         }
230
231         if (vol[key]() !== value) {
232             vol[key](value);
233             vol.ischanged(true);
234         }
235
236         this.emitSaveChange();
237     }
238
239     applyCopyValue(copy: IdlObject, key: string, value: any) {
240         if (copy[key]() !== value) {
241             copy[key](value);
242             copy.ischanged(true);
243         }
244     }
245
246     copyPartChanged(copyNode: HoldingsTreeNode, entry: ComboboxEntry) {
247         const copy = copyNode.target;
248         const part = copyNode.target.parts()[0];
249
250         if (entry) {
251
252             let newPart;
253             if (entry.freetext) {
254                 newPart = this.idl.create('bmp');
255                 newPart.isnew(true);
256                 newPart.record(copy.call_number().record());
257                 newPart.label(entry.label);
258
259             } else {
260
261                 newPart =
262                     this.volcopy.bibParts[copy.call_number().record()]
263                     .filter(p => p.id() === entry.id)[0];
264
265                 // Nothing to change?
266                 if (part && part.id() === newPart.id()) { return; }
267             }
268
269             copy.parts([newPart]);
270             copy.ischanged(true);
271
272         } else if (part) { // Part map no longer needed.
273
274             copy.parts([]);
275             copy.ischanged(true);
276         }
277     }
278
279     batchVolApply() {
280         this.context.volNodes().forEach(volNode => {
281             const vol = volNode.target;
282             if (this.batchVolClass) {
283                 this.applyVolValue(vol, 'label_class', this.batchVolClass.id);
284             }
285             if (this.batchVolPrefix) {
286                 this.applyVolValue(vol, 'prefix', this.batchVolPrefix.id);
287             }
288             if (this.batchVolSuffix) {
289                 this.applyVolValue(vol, 'suffix', this.batchVolSuffix.id);
290             }
291             if (this.batchVolLabel) {
292                 // Use label; could be freetext.
293                 this.applyVolValue(vol, 'label', this.batchVolLabel.label);
294             }
295         });
296     }
297
298     // Focus and select the next editable barcode.
299     selectNextBarcode(id: number, previous?: boolean) {
300         let found = false;
301         let nextId: number = null;
302         let firstId: number = null;
303
304         let copies = this.context.copyList();
305         if (previous) { copies = copies.reverse(); }
306
307         // Find the ID of the next item.  If this is the last item,
308         // loop back to the first item.
309         copies.forEach(copy => {
310             if (nextId !== null) { return; }
311
312             // In case we have to loop back to the first copy.
313             if (firstId === null && this.barcodeCanChange(copy)) {
314                 firstId = copy.id();
315             }
316
317             if (found) {
318                 if (nextId === null && this.barcodeCanChange(copy)) {
319                     nextId = copy.id();
320                 }
321             } else if (copy.id() === id) {
322                 found = true;
323             }
324         });
325
326         this.renderer.selectRootElement(
327                 '#barcode-input-' + (nextId || firstId)).select();
328     }
329
330     barcodeCanChange(copy: IdlObject): boolean {
331         return !this.volcopy.copyStatIsMagic(copy.status());
332     }
333
334     generateBarcodes() {
335         this.autoBarcodeInProgress = true;
336
337         // Autogen only replaces barcodes for items which are in
338         // certain statuses.
339         const copies = this.context.copyList()
340         .filter((copy, idx) => {
341             // During autogen we do not replace the first item,
342             // so it's status is not relevant.
343             return idx === 0 || this.barcodeCanChange(copy);
344         });
345
346         if (copies.length > 1) { // seed barcode will always be present
347             this.proceedWithAutogen(copies)
348             .then(_ => this.autoBarcodeInProgress = false);
349         }
350     }
351
352     proceedWithAutogen(copyList: IdlObject[]): Promise<any> {
353
354         const seedBarcode: string = copyList[0].barcode();
355         copyList.shift(); // Avoid replacing the seed barcode
356
357         const count = copyList.length;
358
359         return this.net.request('open-ils.cat',
360             'open-ils.cat.item.barcode.autogen',
361             this.auth.token(), seedBarcode, count, {
362                 checkdigit: this.volcopy.defaults.values.use_checkdigit,
363                 skip_dupes: true
364             }
365         ).pipe(tap(barcodes => {
366
367             copyList.forEach(copy => {
368                 if (copy.barcode() !== barcodes[0]) {
369                     copy.barcode(barcodes[0]);
370                     copy.ischanged(true);
371                 }
372                 barcodes.shift();
373             });
374
375         })).toPromise();
376     }
377
378     barcodeChanged(copy: IdlObject, barcode: string) {
379
380         if (barcode) {
381             // Scrub leading/trailing spaces from barcodes
382             barcode = barcode.trim();
383             copy.barcode(barcode);
384         }
385
386         copy.ischanged(true);
387         copy._dupe_barcode = false;
388
389         if (!barcode) {
390             this.emitSaveChange();
391             return;
392         }
393
394         if (!this.autoBarcodeInProgress) {
395             // Manual barcode entry requires dupe check
396
397             copy._dupe_barcode = false;
398             this.pcrud.search('acp', {
399                 deleted: 'f',
400                 barcode: barcode,
401                 id: {'!=': copy.id()}
402             }).subscribe(
403                 resp => {
404                     if (resp) { copy._dupe_barcode = true; }
405                 },
406                 err => {},
407                 () => this.emitSaveChange()
408             );
409         }
410     }
411
412     deleteCopy(copyNode: HoldingsTreeNode) {
413
414         if (copyNode.target.isnew()) {
415             // Confirmation not required when deleting brand new copies.
416             this.deleteOneCopy(copyNode);
417             return;
418         }
419
420         this.deleteCopyCount = 1;
421         this.confirmDelCopy.open().toPromise().then(confirmed => {
422             if (confirmed) { this.deleteOneCopy(copyNode); }
423         });
424     }
425
426     deleteOneCopy(copyNode: HoldingsTreeNode) {
427         const targetCopy = copyNode.target;
428
429         const orgNodes = this.context.orgNodes();
430         for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
431             const orgNode = orgNodes[orgIdx];
432
433             for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
434                 const volNode = orgNode.children[volIdx];
435
436                 for (let copyIdx = 0; copyIdx < volNode.children.length; copyIdx++) {
437                     const copy = volNode.children[copyIdx].target;
438
439                     if (copy.id() === targetCopy.id()) {
440                         volNode.children.splice(copyIdx, 1);
441                         if (!copy.isnew()) {
442                             copy.isdeleted(true);
443                             this.context.copiesToDelete.push(copy);
444                         }
445
446                         if (volNode.children.length === 0) {
447                             // When removing the last copy, add a stub copy.
448                             this.addStubCopies();
449                         }
450
451                         return;
452                     }
453                 }
454             }
455         }
456     }
457
458
459     deleteVol(volNode: HoldingsTreeNode) {
460
461         if (volNode.target.isnew()) {
462             // Confirmation not required when deleting brand new vols.
463             this.deleteOneVol(volNode);
464             return;
465         }
466
467         this.deleteVolCount = 1;
468         this.deleteCopyCount = volNode.children.length;
469
470         this.confirmDelVol.open().toPromise().then(confirmed => {
471             if (confirmed) { this.deleteOneVol(volNode); }
472         });
473     }
474
475     deleteOneVol(volNode: HoldingsTreeNode) {
476
477         let deleteVolIdx = null;
478         const targetVol = volNode.target;
479
480         // FOR loops allow for early exit
481         const orgNodes = this.context.orgNodes();
482         for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
483             const orgNode = orgNodes[orgIdx];
484
485             for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
486                 const vol = orgNode.children[volIdx].target;
487
488                 if (vol.id() === targetVol.id()) {
489                     deleteVolIdx = volIdx;
490
491                     if (vol.isnew()) {
492                         // New volumes, which can only have new copies
493                         // may simply be removed from the holdings
494                         // tree to delete them.
495                         break;
496                     }
497
498                     // Mark volume and attached copies as deleted
499                     // and track for later deletion.
500                     targetVol.isdeleted(true);
501                     this.context.volsToDelete.push(targetVol);
502
503                     // When deleting vols, no need to delete the linked
504                     // copies.  They'll be force deleted via the API.
505                 }
506
507                 if (deleteVolIdx !== null) { break; }
508             }
509
510             if (deleteVolIdx !== null) {
511                 orgNode.children.splice(deleteVolIdx, 1);
512                 break;
513             }
514         }
515     }
516
517     displayColumn(field: string): boolean {
518         return this.volcopy.defaults.hidden[field] !== true;
519     }
520
521     canSave(): boolean {
522
523         const copies = this.context.copyList();
524
525         const badCopies = copies.filter(copy => {
526             return copy._dupe_barcode || (!copy.isnew() && !copy.barcode());
527         }).length > 0;
528
529         if (badCopies) { return false; }
530
531         const badVols = this.context.volNodes().filter(volNode => {
532             const vol = volNode.target;
533             return !(
534                 vol.prefix() && vol.label() && vol.suffix && vol.label_class()
535             );
536         }).length > 0;
537
538         return !badVols;
539     }
540
541     // Called any time a change occurs that could affect the
542     // save-ability of the form.
543     emitSaveChange() {
544         setTimeout(() => {
545             this.canSaveChange.emit(this.canSave());
546         });
547     }
548
549     // Given a DOM ID, focus the element after a 0 timeout.
550     focusElement(domId: string) {
551         setTimeout(() => {
552             const node = document.getElementById(domId);
553             if (node) { node.focus(); }
554         });
555     }
556
557
558     toggleBatchVisibility() {
559         this.volcopy.defaults.visible.batch_actions =
560             !this.volcopy.defaults.visible.batch_actions;
561         this.volcopy.saveDefaults();
562     }
563 }
564