]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/cat/volcopy/vol-edit.component.ts
LP1888723 Disable special copy statuses in status selector
[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         // It's possible the loaded data is not strictly allowed,
97         // e.g. empty string call number labels
98         .then(_ => this.emitSaveChange(true));
99     }
100
101     copyStatLabel(copy: IdlObject): string {
102         if (copy) {
103             const statId = copy.status();
104             if (statId in this.volcopy.copyStatuses) {
105                 return this.volcopy.copyStatuses[statId].name();
106             }
107         }
108         return '';
109     }
110
111     recordHasParts(bibId: number): boolean {
112         return this.volcopy.bibParts[bibId] &&
113             this.volcopy.bibParts[bibId].length > 0;
114     }
115
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
121             return 0;
122         }
123         let value = this.flexSettings[column];
124         if (this.expand === column) { value = value * 3; }
125         return value;
126     }
127
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();
133     }
134
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();
142             } else {
143                 break;
144             }
145         }
146     }
147
148     createCopies(volNode: HoldingsTreeNode, count: number) {
149         const copies = [];
150         for (let i = 0; i < count; i++) {
151
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);
157             copies.push(copy);
158         }
159
160         this.volcopy.setCopyStatus(copies);
161     }
162
163     createCopiesFromPopover(volNode: HoldingsTreeNode, popover: any) {
164         this.createCopies(volNode, this.addCopyCount);
165         popover.close();
166         this.addCopyCount = null;
167     }
168
169     createVolsFromPopover(orgNode: HoldingsTreeNode, popover: any) {
170         this.createVols(orgNode, this.addVolCount);
171         popover.close();
172         this.addVolCount = null;
173     }
174
175     createVols(orgNode: HoldingsTreeNode, count: number) {
176         const vols = [];
177         const copies = [];
178         for (let i = 0; i < count; i++) {
179
180             // This will vivify the volNode if needed.
181             const vol = this.volcopy.createStubVol(
182                 this.context.recordId, orgNode.target.id());
183
184             vols.push(vol);
185
186             // Our context assumes copies are fleshed with volumes
187             const copy = this.volcopy.createStubCopy(vol);
188             copy.call_number(vol);
189             copies.push(copy);
190             this.context.findOrCreateCopyNode(copy);
191         }
192
193         this.volcopy.setCopyStatus(copies);
194         this.volcopy.setVolClassLabels(vols);
195     }
196
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();
204             } else {
205                 break;
206             }
207         }
208     }
209
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();
213
214         const copies = [];
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);
220                 copies.push(copy);
221                 this.context.findOrCreateCopyNode(copy);
222             }
223         });
224
225         this.volcopy.setCopyStatus(copies);
226     }
227
228     applyVolValue(vol: IdlObject, key: string, value: any) {
229
230         if (value === null && (key === 'prefix' || key === 'suffix')) {
231             // -1 is the empty prefix/suffix value.
232             value = -1;
233         }
234
235         if (vol[key]() !== value) {
236             vol[key](value);
237             vol.ischanged(true);
238         }
239
240         this.emitSaveChange();
241     }
242
243     applyCopyValue(copy: IdlObject, key: string, value: any) {
244         if (copy[key]() !== value) {
245             copy[key](value);
246             copy.ischanged(true);
247         }
248     }
249
250     copyPartChanged(copyNode: HoldingsTreeNode, entry: ComboboxEntry) {
251         const copy = copyNode.target;
252         const part = copyNode.target.parts()[0];
253
254         if (entry) {
255
256             let newPart;
257             if (entry.freetext) {
258                 newPart = this.idl.create('bmp');
259                 newPart.isnew(true);
260                 newPart.record(copy.call_number().record());
261                 newPart.label(entry.label);
262
263             } else {
264
265                 newPart =
266                     this.volcopy.bibParts[copy.call_number().record()]
267                     .filter(p => p.id() === entry.id)[0];
268
269                 // Nothing to change?
270                 if (part && part.id() === newPart.id()) { return; }
271             }
272
273             copy.parts([newPart]);
274             copy.ischanged(true);
275
276         } else if (part) { // Part map no longer needed.
277
278             copy.parts([]);
279             copy.ischanged(true);
280         }
281     }
282
283     batchVolApply() {
284         this.context.volNodes().forEach(volNode => {
285             const vol = volNode.target;
286             if (this.batchVolClass) {
287                 this.applyVolValue(vol, 'label_class', this.batchVolClass.id);
288             }
289             if (this.batchVolPrefix) {
290                 this.applyVolValue(vol, 'prefix', this.batchVolPrefix.id);
291             }
292             if (this.batchVolSuffix) {
293                 this.applyVolValue(vol, 'suffix', this.batchVolSuffix.id);
294             }
295             if (this.batchVolLabel) {
296                 // Use label; could be freetext.
297                 this.applyVolValue(vol, 'label', this.batchVolLabel.label);
298             }
299         });
300     }
301
302     // Focus and select the next editable barcode.
303     selectNextBarcode(id: number, previous?: boolean) {
304         let found = false;
305         let nextId: number = null;
306         let firstId: number = null;
307
308         let copies = this.context.copyList();
309         if (previous) { copies = copies.reverse(); }
310
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; }
315
316             // In case we have to loop back to the first copy.
317             if (firstId === null && this.barcodeCanChange(copy)) {
318                 firstId = copy.id();
319             }
320
321             if (found) {
322                 if (nextId === null && this.barcodeCanChange(copy)) {
323                     nextId = copy.id();
324                 }
325             } else if (copy.id() === id) {
326                 found = true;
327             }
328         });
329
330         this.renderer.selectRootElement(
331                 '#barcode-input-' + (nextId || firstId)).select();
332     }
333
334     barcodeCanChange(copy: IdlObject): boolean {
335         return !this.volcopy.copyStatIsMagic(copy.status());
336     }
337
338     generateBarcodes() {
339         this.autoBarcodeInProgress = true;
340
341         // Autogen only replaces barcodes for items which are in
342         // certain statuses.
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);
348         });
349
350         if (copies.length > 1) { // seed barcode will always be present
351             this.proceedWithAutogen(copies)
352             .then(_ => this.autoBarcodeInProgress = false);
353         }
354     }
355
356     proceedWithAutogen(copyList: IdlObject[]): Promise<any> {
357
358         const seedBarcode: string = copyList[0].barcode();
359         copyList.shift(); // Avoid replacing the seed barcode
360
361         const count = copyList.length;
362
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,
367                 skip_dupes: true
368             }
369         ).pipe(tap(barcodes => {
370
371             copyList.forEach(copy => {
372                 if (copy.barcode() !== barcodes[0]) {
373                     copy.barcode(barcodes[0]);
374                     copy.ischanged(true);
375                 }
376                 barcodes.shift();
377             });
378
379         })).toPromise();
380     }
381
382     barcodeChanged(copy: IdlObject, barcode: string) {
383
384         if (barcode) {
385             // Scrub leading/trailing spaces from barcodes
386             barcode = barcode.trim();
387             copy.barcode(barcode);
388         }
389
390         copy.ischanged(true);
391         copy._dupe_barcode = false;
392
393         if (!barcode) {
394             this.emitSaveChange();
395             return;
396         }
397
398         if (!this.autoBarcodeInProgress) {
399             // Manual barcode entry requires dupe check
400
401             copy._dupe_barcode = false;
402             this.pcrud.search('acp', {
403                 deleted: 'f',
404                 barcode: barcode,
405                 id: {'!=': copy.id()}
406             }).subscribe(
407                 resp => {
408                     if (resp) { copy._dupe_barcode = true; }
409                 },
410                 err => {},
411                 () => this.emitSaveChange()
412             );
413         }
414     }
415
416     deleteCopy(copyNode: HoldingsTreeNode) {
417
418         if (copyNode.target.isnew()) {
419             // Confirmation not required when deleting brand new copies.
420             this.deleteOneCopy(copyNode);
421             return;
422         }
423
424         this.deleteCopyCount = 1;
425         this.confirmDelCopy.open().toPromise().then(confirmed => {
426             if (confirmed) { this.deleteOneCopy(copyNode); }
427         });
428     }
429
430     deleteOneCopy(copyNode: HoldingsTreeNode) {
431         const targetCopy = copyNode.target;
432
433         const orgNodes = this.context.orgNodes();
434         for (let orgIdx = 0; orgIdx < orgNodes.length; orgIdx++) {
435             const orgNode = orgNodes[orgIdx];
436
437             for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
438                 const volNode = orgNode.children[volIdx];
439
440                 for (let copyIdx = 0; copyIdx < volNode.children.length; copyIdx++) {
441                     const copy = volNode.children[copyIdx].target;
442
443                     if (copy.id() === targetCopy.id()) {
444                         volNode.children.splice(copyIdx, 1);
445                         if (!copy.isnew()) {
446                             copy.isdeleted(true);
447                             this.context.copiesToDelete.push(copy);
448                         }
449
450                         if (volNode.children.length === 0) {
451                             // When removing the last copy, add a stub copy.
452                             this.addStubCopies();
453                         }
454
455                         return;
456                     }
457                 }
458             }
459         }
460     }
461
462
463     deleteVol(volNode: HoldingsTreeNode) {
464
465         if (volNode.target.isnew()) {
466             // Confirmation not required when deleting brand new vols.
467             this.deleteOneVol(volNode);
468             return;
469         }
470
471         this.deleteVolCount = 1;
472         this.deleteCopyCount = volNode.children.length;
473
474         this.confirmDelVol.open().toPromise().then(confirmed => {
475             if (confirmed) { this.deleteOneVol(volNode); }
476         });
477     }
478
479     deleteOneVol(volNode: HoldingsTreeNode) {
480
481         let deleteVolIdx = null;
482         const targetVol = volNode.target;
483
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];
488
489             for (let volIdx = 0; volIdx < orgNode.children.length; volIdx++) {
490                 const vol = orgNode.children[volIdx].target;
491
492                 if (vol.id() === targetVol.id()) {
493                     deleteVolIdx = volIdx;
494
495                     if (vol.isnew()) {
496                         // New volumes, which can only have new copies
497                         // may simply be removed from the holdings
498                         // tree to delete them.
499                         break;
500                     }
501
502                     // Mark volume and attached copies as deleted
503                     // and track for later deletion.
504                     targetVol.isdeleted(true);
505                     this.context.volsToDelete.push(targetVol);
506
507                     // When deleting vols, no need to delete the linked
508                     // copies.  They'll be force deleted via the API.
509                 }
510
511                 if (deleteVolIdx !== null) { break; }
512             }
513
514             if (deleteVolIdx !== null) {
515                 orgNode.children.splice(deleteVolIdx, 1);
516                 break;
517             }
518         }
519     }
520
521     displayColumn(field: string): boolean {
522         return this.volcopy.defaults.hidden[field] !== true;
523     }
524
525     canSave(): boolean {
526
527         const copies = this.context.copyList();
528
529         const badCopies = copies.filter(copy => {
530             return copy._dupe_barcode || (!copy.isnew() && !copy.barcode());
531         }).length > 0;
532
533         if (badCopies) { return false; }
534
535         const badVols = this.context.volNodes().filter(volNode => {
536             const vol = volNode.target;
537             return !(
538                 vol.prefix() && vol.label() && vol.suffix && vol.label_class()
539             );
540         }).length > 0;
541
542         return !badVols;
543     }
544
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();
549
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; }
553
554         setTimeout(() => {
555             this.canSaveChange.emit(saveable);
556         });
557     }
558
559     // Given a DOM ID, focus the element after a 0 timeout.
560     focusElement(domId: string) {
561         setTimeout(() => {
562             const node = document.getElementById(domId);
563             if (node) { node.focus(); }
564         });
565     }
566
567
568     toggleBatchVisibility() {
569         this.volcopy.defaults.visible.batch_actions =
570             !this.volcopy.defaults.visible.batch_actions;
571         this.volcopy.saveDefaults();
572     }
573 }
574