LP1942220: follow-up: ng lint fixes
[evergreen-equinox.git] / Open-ILS / src / eg2 / src / app / staff / acq / picklist / upload.component.ts
1 import {Component, AfterViewInit, Input,
2     ViewChild, OnDestroy} from '@angular/core';
3 import {Router} from '@angular/router';
4 import {tap} from 'rxjs/operators';
5 import {IdlObject} from '@eg/core/idl.service';
6 import {NetService} from '@eg/core/net.service';
7 import {EventService} from '@eg/core/event.service';
8 import {OrgService} from '@eg/core/org.service';
9 import {AuthService} from '@eg/core/auth.service';
10 import {StringComponent} from '@eg/share/string/string.component';
11 import {ToastService} from '@eg/share/toast/toast.service';
12 import {ComboboxComponent,
13     ComboboxEntry} from '@eg/share/combobox/combobox.component';
14 import {VandelayImportSelection,
15   VANDELAY_UPLOAD_PATH} from '@eg/staff/cat/vandelay/vandelay.service';
16 import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
17 import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
18 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
19 import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
20 import {ServerStoreService} from '@eg/core/server-store.service';
21 import {PicklistUploadService} from './upload.service';
22
23
24 const TEMPLATE_SETTING_NAME = 'eg.acq.picklist.upload.templates';
25
26 const TEMPLATE_ATTRS = [
27     'createPurchaseOrder',
28     'activatePurchaseOrder',
29     'selectedProvider',
30     'orderingAgency',
31     'selectedFiscalYear',
32     'loadItems',
33     'selectedBibSource',
34     'selectedMatchSet',
35     'mergeOnExact',
36     'importNonMatching',
37     'mergeOnBestMatch',
38     'mergeOnSingleMatch',
39     'selectedMergeProfile',
40     'selectedFallThruMergeProfile',
41     'minQualityRatio'
42 ];
43
44 const ORG_SETTINGS = [
45     'acq.upload.default.activate_po',
46     'acq.upload.default.create_po',
47     'acq.upload.default.provider',
48     'acq.upload.default.vandelay.import_non_matching',
49     'acq.upload.default.vandelay.load_item_for_imported',
50     'acq.upload.default.vandelay.low_quality_fall_thru_profile',
51     'acq.upload.default.vandelay.match_set',
52     'acq.upload.default.vandelay.merge_on_best',
53     'acq.upload.default.vandelay.merge_on_exact',
54     'acq.upload.default.vandelay.merge_on_single',
55     'acq.upload.default.vandelay.merge_profile',
56     'acq.upload.default.vandelay.quality_ratio'
57 ];
58
59
60 @Component({
61   selector: 'eg-acq-upload',
62   templateUrl: './upload.component.html'
63 })
64 export class UploadComponent implements AfterViewInit, OnDestroy {
65
66     // mode can be one of
67     //  upload:          actually upload and process a MARC order file
68     //  getImportParams: gather import parameters to use when creating
69     //                   assets for a purchase order; the invoker
70     //                   would do the actual asset creation
71     @Input() mode = 'upload';
72
73     @Input() customAction: (args: any) => void;
74     customActionProcessing = false;
75
76     settings: Object = {};
77     recordType: string;
78     selectedQueue: ComboboxEntry;
79
80
81     activeSelectionListId: number;
82     activeQueueId: number;
83     orderingAgency: number;
84     selectedFiscalYear: number;
85     selectedSelectionList: ComboboxEntry;
86     selectedBibSource: number;
87     selectedProvider: number;
88     selectedMatchSet: number;
89     importDefId: number;
90     selectedMergeProfile: number;
91     selectedFallThruMergeProfile: number;
92     selectedFile: File;
93     newPO: number;
94
95     defaultMatchSet: string;
96
97     createPurchaseOrder: boolean;
98     activatePurchaseOrder: boolean;
99     loadItems: boolean;
100
101     importNonMatching: boolean;
102     mergeOnExact: boolean;
103     mergeOnSingleMatch: boolean;
104     mergeOnBestMatch: boolean;
105     minQualityRatio: number;
106
107     isUploading: boolean;
108     uploadProcessing: boolean;
109     uploadError: boolean;
110     uploadErrorCode: string;
111     uploadErrorText: string;
112     uploadComplete: boolean;
113
114     // Generated by the server
115     sessionKey: string;
116
117     selectedTemplate: string;
118     formTemplates: {[name: string]: any};
119     newTemplateName: string;
120
121     @ViewChild('fileSelector', { static: false }) private fileSelector;
122     @ViewChild('uploadProgress', { static: true })
123         private uploadProgress: ProgressInlineComponent;
124
125     @ViewChild('formTemplateSelector', { static: true })
126         private formTemplateSelector: ComboboxComponent;
127     @ViewChild('bibSourceSelector', { static: true })
128         private bibSourceSelector: ComboboxComponent;
129     @ViewChild('providerSelector', {static: false})
130         private providerSelector: ComboboxComponent;
131     @ViewChild('fiscalYearSelector', { static: false })
132         private fiscalYearSelector: ComboboxComponent;
133     @ViewChild('selectionListSelector', { static: true })
134         private selectionListSelector: ComboboxComponent;
135     @ViewChild('matchSetSelector', { static: true })
136         private matchSetSelector: ComboboxComponent;
137     @ViewChild('mergeProfileSelector', { static: true })
138         private mergeProfileSelector: ComboboxComponent;
139     @ViewChild('fallThruMergeProfileSelector', { static: true })
140         private fallThruMergeProfileSelector: ComboboxComponent;
141     @ViewChild('dupeQueueAlert', { static: true })
142         private dupeQueueAlert: AlertDialogComponent;
143     @ViewChild('loadMarcOrderTemplateSavedString', { static: false })
144         private loadMarcOrderTemplateSavedString: StringComponent;
145     @ViewChild('loadMarcOrderTemplateDeletedString', { static: false })
146         private loadMarcOrderTemplateDeletedString: StringComponent;
147     @ViewChild('loadMarcOrderTemplateSetAsDefaultString', { static: false })
148         private loadMarcOrderTemplateSetAsDefaultString: StringComponent;
149
150
151     constructor(
152         private http: HttpClient,
153         private router: Router,
154         private toast: ToastService,
155         private evt: EventService,
156         private net: NetService,
157         private auth: AuthService,
158         private org: OrgService,
159         private store: ServerStoreService,
160         private vlagent: PicklistUploadService
161     ) {
162         // force a reload of the component if we navigate to it
163         // from itself
164         this.router.routeReuseStrategy.shouldReuseRoute = () => {
165             return false;
166         };
167         this.applyDefaults();
168         this.applySettings();
169     }
170
171     applySettings(): Promise<any> {
172         return this.store.getItemBatch(ORG_SETTINGS)
173         .then(settings => {
174             this.createPurchaseOrder = settings['acq.upload.default.create_po'];
175             this.activatePurchaseOrder = settings['acq.upload.default.activate_po'];
176             this.selectedProvider = Number(settings['acq.upload.default.provider']);
177             this.importNonMatching = settings['acq.upload.default.vandelay.import_non_matching'];
178             this.loadItems = settings['acq.upload.default.vandelay.load_item_for_imported'];
179             this.selectedFallThruMergeProfile = Number(settings['acq.upload.default.vandelay.low_quality_fall_thru_profile']);
180             this.selectedMatchSet = Number(settings['acq.upload.default.vandelay.match_set']);
181             this.mergeOnBestMatch = settings['acq.upload.default.vandelay.merge_on_best'];
182             this.mergeOnExact = settings['acq.upload.default.vandelay.merge_on_exact'];
183             this.mergeOnSingleMatch = settings['acq.upload.default.vandelay.merge_on_single'];
184             this.selectedMergeProfile = Number(settings['acq.upload.default.vandelay.merge_profile']);
185             this.minQualityRatio = Number(settings['acq.upload.default.vandelay.quality_ratio']);
186         });
187     }
188     applyDefaults() {
189         this.minQualityRatio = 0;
190         this.recordType = 'bib';
191         this.formTemplates = {};
192         if (this.vlagent.importSelection) {
193
194             if (!this.vlagent.importSelection.queue) {
195                 // Incomplete import selection, clear it.
196                 this.vlagent.importSelection = null;
197                 return;
198             }
199
200             const queue = this.vlagent.importSelection.queue;
201             this.selectedMatchSet = queue.match_set();
202
203         }
204     }
205
206     ngAfterViewInit() {
207         this.loadStartupData();
208     }
209
210     ngOnDestroy() {
211         this.clearSelection();
212     }
213
214     importSelection(): VandelayImportSelection {
215         return this.vlagent.importSelection;
216     }
217
218     loadStartupData(): Promise<any> {
219
220
221         const promises = [
222             this.vlagent.getMergeProfiles(),
223             this.vlagent.getAllQueues('bib'),
224             this.vlagent.getMatchSets('bib'),
225             this.vlagent.getBibSources(),
226             this.vlagent.getFiscalYears(this.auth.user().ws_ou()).then( years => {
227                 this.vlagent.getDefaultFiscalYear(this.auth.user().ws_ou()).then(y => {
228                     this.selectedFiscalYear = y.id();
229                     if (this.fiscalYearSelector) {
230                         this.fiscalYearSelector.applyEntryId(this.selectedFiscalYear);
231                     }
232                 });
233             }),
234             this.vlagent.getSelectionLists(),
235             this.vlagent.getItemImportDefs(),
236             this.org.settings(['vandelay.default_match_set']).then(
237                 s => this.defaultMatchSet = s['vandelay.default_match_set']),
238             this.loadTemplates()
239         ];
240
241         return Promise.all(promises);
242     }
243
244
245     orgOnChange(org: IdlObject) {
246         this.orderingAgency = org.id();
247         this.vlagent.getFiscalYears(this.orderingAgency).then( years => {
248             this.vlagent.getDefaultFiscalYear(this.orderingAgency).then(
249                 y => { this.selectedFiscalYear = y.id(); this.fiscalYearSelector.applyEntryId(this.selectedFiscalYear); }
250             );
251         });
252     }
253
254     loadTemplates() {
255         this.store.getItem(TEMPLATE_SETTING_NAME).then(
256             templates => {
257                 this.formTemplates = templates || {};
258
259                 Object.keys(this.formTemplates).forEach(name => {
260                     if (this.formTemplates[name].default) {
261                         this.selectedTemplate = name;
262                     }
263                 });
264             }
265         );
266     }
267
268     formatTemplateEntries(): ComboboxEntry[] {
269         const entries = [];
270
271         Object.keys(this.formTemplates || {}).forEach(
272             name => entries.push({id: name, label: name}));
273
274         return entries;
275     }
276
277     formatEntries(etype: string): ComboboxEntry[] {
278         const rtype = this.recordType;
279         let list;
280
281         switch (etype) {
282             case 'bibSources':
283                 return (this.vlagent.bibSources || []).map(
284                     s => {
285                         return {id: s.id(), label: s.source()};
286                     });
287
288             case 'fiscalYears':
289                 return (this.vlagent.fiscalYears || []).map(
290                     fy => {
291                         return {id: fy.id(), label: fy.year()};
292                        });
293                 break;
294
295             case 'selectionLists':
296                  list = this.vlagent.selectionLists;
297                  break;
298
299             case 'activeQueues':
300                 list = (this.vlagent.allQueues[rtype] || []);
301                 break;
302
303             case 'matchSets':
304                 list = this.vlagent.matchSets['bib'];
305                 break;
306
307
308             case 'importItemDefs':
309                 list = this.vlagent.importItemAttrDefs;
310                 break;
311
312             case 'mergeProfiles':
313                 list = this.vlagent.mergeProfiles;
314                 break;
315         }
316
317         return (list || []).map(item => {
318             return {id: item.id(), label: item.name()};
319         });
320     }
321
322     selectEntry($event: ComboboxEntry, etype: string) {
323         const id = $event ? $event.id : null;
324
325         switch (etype) {
326             case 'recordType':
327                 this.recordType = id;
328                 break;
329
330             case 'bibSources':
331                 this.selectedBibSource = id;
332                 break;
333
334             case 'fiscalYears':
335                 this.selectedFiscalYear = id;
336                 break;
337
338             case 'selectionLists':
339                 this.selectedSelectionList = id;
340                 break;
341
342             case 'matchSets':
343                 this.selectedMatchSet = id;
344                 break;
345
346
347             case 'mergeProfiles':
348                 this.selectedMergeProfile = id;
349                 break;
350
351             case 'FallThruMergeProfile':
352                 this.selectedFallThruMergeProfile = id;
353                 break;
354         }
355     }
356
357     fileSelected($event) {
358        this.selectedFile = $event.target.files[0];
359     }
360
361     hasNeededData(): boolean {
362         if (this.mode === 'getImportParams') {
363             return this.selectedQueue ? true : false;
364         }
365         return this.selectedQueue &&
366         Boolean(this.selectedFile) &&
367         Boolean(this.selectedFiscalYear) &&
368         Boolean(this.selectedProvider) &&
369         Boolean(this.orderingAgency);
370     }
371
372     upload() {
373         this.sessionKey = null;
374         this.isUploading = true;
375         this.uploadComplete = false;
376         this.resetProgressBars();
377
378         this.resolveSelectionList(),
379         this.resolveQueue()
380         .then(
381             queueId => {
382                 this.activeQueueId = queueId;
383                 return this.uploadFile();
384             },
385             err => Promise.reject('queue create failed')
386         ).then(
387             ok => this.processUpload(),
388             err => Promise.reject('process spool failed')
389         ).then(
390             ok => {
391                 this.isUploading = false;
392                 this.uploadComplete = true;
393             },
394             err => {
395                 console.log('file upload failed: ', err);
396                 this.isUploading = false;
397                 this.resetProgressBars();
398
399             }
400         );
401     }
402
403     // helper method to return the year string rather than the FY ID
404     // TODO: can remove this once fiscal years are better managed
405     _getFiscalYearLabel(): string {
406         if (this.selectedFiscalYear) {
407             const found =  (this.vlagent.fiscalYears || []).find(x => x.id() === this.selectedFiscalYear);
408             return found ? found.year() : '';
409         } else {
410             return '';
411         }
412     }
413
414     performCustomAction() {
415
416         const vandelayOptions = {
417             match_set: this.selectedMatchSet,
418             import_no_match: this.importNonMatching,
419             auto_overlay_exact: this.mergeOnExact,
420             auto_overlay_best_match: this.mergeOnBestMatch,
421             auto_overlay_1match: this.mergeOnSingleMatch,
422             merge_profile: this.selectedMergeProfile,
423             fall_through_merge_profile: this.selectedFallThruMergeProfile,
424             match_quality_ratio: this.minQualityRatio,
425             bib_source: this.selectedBibSource,
426             create_assets: this.loadItems,
427             queue_name: this.selectedQueue.label
428         };
429
430         const args = {
431             provider: this.selectedProvider,
432             ordering_agency: this.orderingAgency,
433             create_po: this.createPurchaseOrder,
434             activate_po: this.activatePurchaseOrder,
435             fiscal_year: this._getFiscalYearLabel(),
436             picklist: this.activeSelectionListId,
437             vandelay: vandelayOptions
438         };
439
440         this.customActionProcessing = true;
441         this.customAction(args);
442     }
443
444     resetProgressBars() {
445         this.uploadProgress.update({value: 0, max: 1});
446     }
447
448     resolveQueue(): Promise<number> {
449
450         if (this.selectedQueue.freetext) {
451             return this.vlagent.createQueue(
452                 this.selectedQueue.label,
453                 this.recordType,
454                 this.importDefId,
455                 this.selectedMatchSet,
456             ).then(
457                 id => id,
458                 err => {
459                     const evt = this.evt.parse(err);
460                     if (evt) {
461                         if (evt.textcode.match(/QUEUE_EXISTS/)) {
462                             this.dupeQueueAlert.open();
463                         } else {
464                             alert(evt); // server error
465                         }
466                     }
467
468                     return Promise.reject('Queue Create Failed');
469                 }
470             );
471         } else {
472             return Promise.resolve(this.selectedQueue.id);
473         }
474     }
475
476     resolveSelectionList(): Promise<any> {
477         if (!this.selectedSelectionList) {
478             return Promise.resolve();
479         }
480         if (this.selectedSelectionList.id) {
481             this.activeSelectionListId = this.selectedSelectionList.id;
482         }
483         if (this.selectedSelectionList.freetext) {
484
485             return this.vlagent.createSelectionList(
486                 this.selectedSelectionList.label,
487                 this.orderingAgency
488             ).then(
489                 value => this.activeSelectionListId = value
490             );
491         }
492         return Promise.resolve(this.activeSelectionListId);
493     }
494
495     uploadFile(): Promise<any> {
496
497         if (this.vlagent.importSelection) {
498             return Promise.resolve();
499         }
500
501         const formData: FormData = new FormData();
502
503         formData.append('ses', this.auth.token());
504         formData.append('marc_upload',
505             this.selectedFile, this.selectedFile.name);
506
507         if (this.selectedBibSource) {
508             formData.append('bib_source', '' + this.selectedBibSource);
509         }
510
511         const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
512             {reportProgress: true, responseType: 'text'});
513
514         return this.http.request(req).pipe(tap(
515             evt => {
516                 if (evt.type === HttpEventType.UploadProgress) {
517                     this.uploadProgress.update(
518                         {value: evt.loaded, max: evt.total});
519
520                 } else if (evt instanceof HttpResponse) {
521                     this.sessionKey = evt.body as string;
522                     console.log(
523                         'vlagent file uploaded OK with key ' + this.sessionKey);
524                 }
525             },
526
527             (err: HttpErrorResponse) => {
528                 console.error(err);
529                 this.toast.danger(err.error);
530             }
531         )).toPromise();
532     }
533
534     processUpload():  Promise<any> {
535
536         this.uploadProcessing = true;
537         this.uploadError = false;
538
539         if (this.vlagent.importSelection) {
540             return Promise.resolve();
541         }
542
543         const spoolType = this.recordType;
544
545         const vandelayOptions = {
546             match_set: this.selectedMatchSet,
547             import_no_match: this.importNonMatching,
548             auto_overlay_exact: this.mergeOnExact,
549             auto_overlay_best_match: this.mergeOnBestMatch,
550             auto_overlay_1match: this.mergeOnSingleMatch,
551             merge_profile: this.selectedMergeProfile,
552             fall_through_merge_profile: this.selectedFallThruMergeProfile,
553             match_quality_ratio: this.minQualityRatio,
554             bib_source: this.selectedBibSource,
555             create_assets: this.loadItems,
556             queue_name: this.selectedQueue.label
557         };
558
559         const args = {
560             provider: this.selectedProvider,
561             ordering_agency: this.orderingAgency,
562             create_po: this.createPurchaseOrder,
563             activate_po: this.activatePurchaseOrder,
564             fiscal_year: this._getFiscalYearLabel(),
565             picklist: this.activeSelectionListId,
566             vandelay: vandelayOptions
567         };
568
569         const method = `open-ils.acq.process_upload_records`;
570
571         return new Promise((resolve, reject) => {
572             this.net.request(
573                 'open-ils.acq', method,
574                 this.auth.token(), this.sessionKey, args
575             ).subscribe(
576                 progress => {
577                     const resp = this.evt.parse(progress);
578                     console.log(progress);
579                     if (resp) {
580                         this.uploadError = true;
581                         this.uploadErrorCode = resp.textcode;
582                         this.uploadErrorText = resp.payload;
583                         this.uploadProcessing = false;
584                         this.uploadComplete = true;
585                         return reject();
586                     }
587                     if (progress.complete) {
588                         this.uploadProcessing = false;
589                         this.uploadComplete = true;
590                     }
591                     if (progress.purchase_order) {this.newPO = progress.purchase_order.id(); }
592                 }
593             );
594         });
595     }
596
597     clearSelection() {
598         this.vlagent.importSelection = null;
599         this.activeSelectionListId = null;
600     }
601
602
603     saveTemplate() {
604
605         const template = {};
606         TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
607
608         this.formTemplates[this.selectedTemplate] = template;
609         this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates).then(x =>
610             this.loadMarcOrderTemplateSavedString.current()
611                 .then(str => this.toast.success(str))
612         );
613     }
614
615     markTemplateDefault() {
616
617         Object.keys(this.formTemplates).forEach(
618             name => delete this.formTemplates[name].default
619         );
620
621         this.formTemplates[this.selectedTemplate].default = true;
622
623         this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates).then(x =>
624             this.loadMarcOrderTemplateSetAsDefaultString.current()
625                 .then(str => this.toast.success(str))
626         );
627     }
628
629     templateSelectorChange(entry: ComboboxEntry) {
630
631         if (!entry) {
632             this.selectedTemplate = '';
633             return;
634         }
635
636         this.selectedTemplate = entry.label; // label == name
637
638         if (entry.freetext) {
639             return;
640         }
641
642         const template = this.formTemplates[entry.id];
643
644         TEMPLATE_ATTRS.forEach(key => this[key] = template[key]);
645
646         this.bibSourceSelector.applyEntryId(this.selectedBibSource);
647         this.matchSetSelector.applyEntryId(this.selectedMatchSet);
648         if (this.providerSelector) {
649             this.providerSelector.selectedId = this.selectedProvider;
650         }
651         if (this.fiscalYearSelector) {
652            this.fiscalYearSelector.applyEntryId(this.selectedFiscalYear);
653         }
654         this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile);
655         this.fallThruMergeProfileSelector.applyEntryId(this.selectedFallThruMergeProfile);
656     }
657
658     deleteTemplate() {
659         delete this.formTemplates[this.selectedTemplate];
660         this.formTemplateSelector.selected = null;
661         this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates).then(x =>
662             this.loadMarcOrderTemplateDeletedString.current()
663                 .then(str => this.toast.success(str))
664         );
665     }
666 }
667