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