]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.component.ts
LP#1929749: fix lint
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / acq / picklist / upload.component.ts
1 import {Component, OnInit, AfterViewInit, Input,
2     ViewChild, OnDestroy} from '@angular/core';
3 import {Subject} from 'rxjs';
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 {ToastService} from '@eg/share/toast/toast.service';
11 import {ComboboxComponent,
12     ComboboxEntry} from '@eg/share/combobox/combobox.component';
13 import {VandelayImportSelection,
14   VANDELAY_UPLOAD_PATH} from '@eg/staff/cat/vandelay/vandelay.service';
15 import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
16 import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
17 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
18 import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
19 import {ServerStoreService} from '@eg/core/server-store.service';
20 import {PicklistUploadService} from './upload.service';
21 import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
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   templateUrl: './upload.component.html'
62 })
63 export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
64
65     settings: Object = {};
66     recordType: string;
67     selectedQueue: ComboboxEntry;
68
69
70     activeSelectionListId: number;
71     activeQueueId: number;
72     orderingAgency: number;
73     selectedFiscalYear: number;
74     selectedSelectionList: ComboboxEntry;
75     selectedBibSource: number;
76     selectedProvider: number;
77     selectedMatchSet: number;
78     importDefId: number;
79     selectedMergeProfile: number;
80     selectedFallThruMergeProfile: number;
81     selectedFile: File;
82     newPO: number;
83
84     defaultMatchSet: string;
85
86     createPurchaseOrder: boolean;
87     activatePurchaseOrder: boolean;
88     loadItems: boolean;
89
90     importNonMatching: boolean;
91     mergeOnExact: boolean;
92     mergeOnSingleMatch: boolean;
93     mergeOnBestMatch: boolean;
94     minQualityRatio: number;
95
96     isUploading: boolean;
97     uploadProcessing: boolean;
98     uploadComplete: boolean;
99
100     // Generated by the server
101     sessionKey: string;
102
103     selectedTemplate: string;
104     formTemplates: {[name: string]: any};
105     newTemplateName: string;
106
107     @ViewChild('fileSelector', { static: false }) private fileSelector;
108     @ViewChild('uploadProgress', { static: true })
109         private uploadProgress: ProgressInlineComponent;
110
111     @ViewChild('formTemplateSelector', { static: true })
112         private formTemplateSelector: ComboboxComponent;
113     @ViewChild('bibSourceSelector', { static: true })
114         private bibSourceSelector: ComboboxComponent;
115     @ViewChild('providerSelector', {static: true})
116         private providerSelector: ComboboxComponent;
117     @ViewChild('fiscalYearSelector', { static: true })
118         private fiscalYearSelector: ComboboxComponent;
119     @ViewChild('selectionListSelector', { static: true })
120         private selectionListSelector: ComboboxComponent;
121     @ViewChild('matchSetSelector', { static: true })
122         private matchSetSelector: ComboboxComponent;
123     @ViewChild('mergeProfileSelector', { static: true })
124         private mergeProfileSelector: ComboboxComponent;
125     @ViewChild('fallThruMergeProfileSelector', { static: true })
126         private fallThruMergeProfileSelector: ComboboxComponent;
127     @ViewChild('dupeQueueAlert', { static: true })
128         private dupeQueueAlert: AlertDialogComponent;
129
130     constructor(
131         private http: HttpClient,
132         private toast: ToastService,
133         private evt: EventService,
134         private net: NetService,
135         private auth: AuthService,
136         private org: OrgService,
137         private store: ServerStoreService,
138         private vlagent: PicklistUploadService
139     ) {
140         this.applyDefaults();
141         this.applySettings();
142     }
143
144     applySettings(): Promise<any> {
145         return this.store.getItemBatch(ORG_SETTINGS)
146         .then(settings => {
147             this.createPurchaseOrder = settings['acq.upload.default.create_po'];
148             this.activatePurchaseOrder = settings['acq.upload.default.activate_po'];
149             this.selectedProvider = Number(settings['acq.upload.default.provider']);
150             this.importNonMatching = settings['acq.upload.default.vandelay.import_non_matching'];
151             this.loadItems = settings['acq.upload.default.vandelay.load_item_for_imported'];
152             this.selectedFallThruMergeProfile = Number(settings['acq.upload.default.vandelay.low_quality_fall_thru_profile']);
153             this.selectedMatchSet = Number(settings['acq.upload.default.vandelay.match_set']);
154             this.mergeOnBestMatch = settings['acq.upload.default.vandelay.merge_on_best'];
155             this.mergeOnExact = settings['acq.upload.default.vandelay.merge_on_exact'];
156             this.mergeOnSingleMatch = settings['acq.upload.default.vandelay.merge_on_single'];
157             this.selectedMergeProfile = Number(settings['acq.upload.default.vandelay.merge_profile']);
158             this.minQualityRatio = Number(settings['acq.upload.default.vandelay.quality_ratio']);
159         });
160     }
161     applyDefaults() {
162         this.minQualityRatio = 0;
163         this.recordType = 'bib';
164         this.formTemplates = {};
165         if (this.vlagent.importSelection) {
166
167             if (!this.vlagent.importSelection.queue) {
168                 // Incomplete import selection, clear it.
169                 this.vlagent.importSelection = null;
170                 return;
171             }
172
173             const queue = this.vlagent.importSelection.queue;
174             this.selectedMatchSet = queue.match_set();
175
176         }
177     }
178
179     ngOnInit() {}
180
181     ngAfterViewInit() {
182         this.loadStartupData();
183     }
184
185     ngOnDestroy() {
186         this.clearSelection();
187     }
188
189     importSelection(): VandelayImportSelection {
190         return this.vlagent.importSelection;
191     }
192
193     loadStartupData(): Promise<any> {
194
195
196         const promises = [
197             this.vlagent.getMergeProfiles(),
198             this.vlagent.getAllQueues('bib'),
199             this.vlagent.getMatchSets('bib'),
200             this.vlagent.getBibSources(),
201             this.vlagent.getFiscalYears(),
202             this.vlagent.getProvidersList(),
203             this.vlagent.getSelectionLists(),
204             this.vlagent.getItemImportDefs(),
205            this.org.settings(['vandelay.default_match_set']).then(
206                s => this.defaultMatchSet = s['vandelay.default_match_set']),
207             this.loadTemplates()
208         ];
209
210         return Promise.all(promises);
211     }
212
213
214     orgOnChange(org: IdlObject) {
215         this.orderingAgency = org.id();
216     }
217
218     loadTemplates() {
219         this.store.getItem(TEMPLATE_SETTING_NAME).then(
220             templates => {
221                 this.formTemplates = templates || {};
222
223                 Object.keys(this.formTemplates).forEach(name => {
224                     if (this.formTemplates[name].default) {
225                         this.selectedTemplate = name;
226                     }
227                 });
228             }
229         );
230     }
231
232     formatTemplateEntries(): ComboboxEntry[] {
233         const entries = [];
234
235         Object.keys(this.formTemplates || {}).forEach(
236             name => entries.push({id: name, label: name}));
237
238         return entries;
239     }
240
241     formatEntries(etype: string): ComboboxEntry[] {
242         const rtype = this.recordType;
243         let list;
244
245         switch (etype) {
246             case 'bibSources':
247                 return (this.vlagent.bibSources || []).map(
248                     s => {
249                         return {id: s.id(), label: s.source()};
250                     });
251
252             case 'providersList':
253                 return (this.vlagent.providersList || []).map(
254                     p => {
255                         return {id: p.id(), label: p.code()};
256                     });
257
258             case 'fiscalYears':
259                 return (this.vlagent.fiscalYears || []).map(
260                     fy => {
261                         return {id: fy.id(), label: fy.year()};
262                        });
263                 break;
264
265             case 'selectionLists':
266                  list = this.vlagent.selectionLists;
267                  break;
268
269             case 'activeQueues':
270                 list = (this.vlagent.allQueues[rtype] || []);
271                 break;
272
273             case 'matchSets':
274                 list = this.vlagent.matchSets['bib'];
275                 break;
276
277
278             case 'importItemDefs':
279                 list = this.vlagent.importItemAttrDefs;
280                 break;
281
282             case 'mergeProfiles':
283                 list = this.vlagent.mergeProfiles;
284                 break;
285         }
286
287         return (list || []).map(item => {
288             return {id: item.id(), label: item.name()};
289         });
290     }
291
292     selectEntry($event: ComboboxEntry, etype: string) {
293         const id = $event ? $event.id : null;
294
295         switch (etype) {
296             case 'recordType':
297                 this.recordType = id;
298                 break;
299
300             case 'providersList':
301                 this.selectedProvider = id;
302                 break;
303
304             case 'bibSources':
305                 this.selectedBibSource = id;
306                 break;
307
308             case 'fiscalYears':
309                 this.selectedFiscalYear = id;
310                 break;
311
312             case 'selectionLists':
313                 this.selectedSelectionList = id;
314                 break;
315
316             case 'matchSets':
317                 this.selectedMatchSet = id;
318                 break;
319
320
321             case 'mergeProfiles':
322                 this.selectedMergeProfile = id;
323                 break;
324
325             case 'FallThruMergeProfile':
326                 this.selectedFallThruMergeProfile = id;
327                 break;
328         }
329     }
330
331     fileSelected($event) {
332        this.selectedFile = $event.target.files[0];
333     }
334
335     hasNeededData(): boolean {
336         return this.selectedQueue &&
337         Boolean(this.selectedFile) &&
338         Boolean(this.selectedFiscalYear) &&
339         Boolean(this.selectedProvider) &&
340         Boolean(this.orderingAgency);
341     }
342
343     upload() {
344         this.sessionKey = null;
345         this.isUploading = true;
346         this.uploadComplete = false;
347         this.resetProgressBars();
348
349         this.resolveSelectionList(),
350         this.resolveQueue()
351         .then(
352             queueId => {
353                 this.activeQueueId = queueId;
354                 return this.uploadFile();
355             },
356             err => Promise.reject('queue create failed')
357         ).then(
358             ok => this.processUpload(),
359             err => Promise.reject('process spool failed')
360         ).then(
361             ok => {
362                 this.isUploading = false;
363                 this.uploadComplete = true;
364             },
365             err => {
366                 console.log('file upload failed: ', err);
367                 this.isUploading = false;
368                 this.resetProgressBars();
369
370             }
371         );
372     }
373
374     resetProgressBars() {
375         this.uploadProgress.update({value: 0, max: 1});
376     }
377
378     resolveQueue(): Promise<number> {
379
380         if (this.selectedQueue.freetext) {
381             return this.vlagent.createQueue(
382                 this.selectedQueue.label,
383                 this.recordType,
384                 this.importDefId,
385                 this.selectedMatchSet,
386             ).then(
387                 id => id,
388                 err => {
389                     const evt = this.evt.parse(err);
390                     if (evt) {
391                         if (evt.textcode.match(/QUEUE_EXISTS/)) {
392                             this.dupeQueueAlert.open();
393                         } else {
394                             alert(evt); // server error
395                         }
396                     }
397
398                     return Promise.reject('Queue Create Failed');
399                 }
400             );
401         } else {
402             return Promise.resolve(this.selectedQueue.id);
403         }
404     }
405
406     resolveSelectionList(): Promise<any> {
407         if (!this.selectedSelectionList) {
408             return Promise.resolve();
409         }
410         if (this.selectedSelectionList.id) {
411             this.activeSelectionListId = this.selectedSelectionList.id;
412         }
413         if (this.selectedSelectionList.freetext) {
414
415             return this.vlagent.createSelectionList(
416                 this.selectedSelectionList.label,
417                 this.orderingAgency
418             ).then(
419                 value => this.activeSelectionListId = value
420             );
421         }
422         return Promise.resolve(this.activeSelectionListId);
423     }
424
425     uploadFile(): Promise<any> {
426
427         if (this.vlagent.importSelection) {
428             return Promise.resolve();
429         }
430
431         const formData: FormData = new FormData();
432
433         formData.append('ses', this.auth.token());
434         formData.append('marc_upload',
435             this.selectedFile, this.selectedFile.name);
436
437         if (this.selectedBibSource) {
438             formData.append('bib_source', '' + this.selectedBibSource);
439         }
440
441         const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
442             {reportProgress: true, responseType: 'text'});
443
444         return this.http.request(req).pipe(tap(
445             evt => {
446                 if (evt.type === HttpEventType.UploadProgress) {
447                     this.uploadProgress.update(
448                         {value: evt.loaded, max: evt.total});
449
450                 } else if (evt instanceof HttpResponse) {
451                     this.sessionKey = evt.body as string;
452                     console.log(
453                         'vlagent file uploaded OK with key ' + this.sessionKey);
454                 }
455             },
456
457             (err: HttpErrorResponse) => {
458                 console.error(err);
459                 this.toast.danger(err.error);
460             }
461         )).toPromise();
462     }
463
464     processUpload():  Promise<any> {
465
466         this.uploadProcessing = true;
467
468         if (this.vlagent.importSelection) {
469             return Promise.resolve();
470         }
471
472         const spoolType = this.recordType;
473
474         const vandelayOptions = {
475             import_no_match: this.importNonMatching,
476             auto_overlay_exact: this.mergeOnExact,
477             auto_overlay_best_match: this.mergeOnBestMatch,
478             auto_overlay_1match: this.mergeOnSingleMatch,
479             merge_profile: this.selectedMergeProfile,
480             fall_through_merge_profile: this.selectedFallThruMergeProfile,
481             match_quality_ratio: this.minQualityRatio,
482             bib_source: this.selectedBibSource,
483             create_assets: this.loadItems,
484             queue_name: this.selectedQueue.label
485         };
486
487         const args = {
488             provider: this.selectedProvider,
489             ordering_agency: this.orderingAgency,
490             create_po: this.createPurchaseOrder,
491             activate_po: this.activatePurchaseOrder,
492             fiscal_year: this.selectedFiscalYear,
493             picklist: this.activeSelectionListId,
494             vandelay: vandelayOptions
495         };
496
497         const method = `open-ils.acq.process_upload_records`;
498
499         return new Promise((resolve, reject) => {
500             this.net.request(
501                 'open-ils.acq', method,
502                 this.auth.token(), this.sessionKey, args
503             ).subscribe(
504                 progress => {
505                     const resp = this.evt.parse(progress);
506                     console.log(progress);
507                     if (resp) { console.error(resp); return reject(); }
508                     if (progress.complete) {
509                         this.uploadProcessing = false;
510                         this.uploadComplete = true;
511                     }
512                     if (progress.purchase_order) {this.newPO = progress.purchase_order.id(); }
513                 }
514             );
515         });
516     }
517
518     clearSelection() {
519         this.vlagent.importSelection = null;
520         this.activeSelectionListId = null;
521     }
522
523
524     saveTemplate() {
525
526         const template = {};
527         TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
528
529         console.debug('Saving import profile', template);
530
531         this.formTemplates[this.selectedTemplate] = template;
532         return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
533     }
534
535     markTemplateDefault() {
536
537         Object.keys(this.formTemplates).forEach(
538             name => delete this.formTemplates.default
539         );
540
541         this.formTemplates[this.selectedTemplate].default = true;
542
543         return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
544     }
545
546     templateSelectorChange(entry: ComboboxEntry) {
547
548         if (!entry) {
549             this.selectedTemplate = '';
550             return;
551         }
552
553         this.selectedTemplate = entry.label; // label == name
554
555         if (entry.freetext) {
556             return;
557         }
558
559         const template = this.formTemplates[entry.id];
560
561         TEMPLATE_ATTRS.forEach(key => this[key] = template[key]);
562
563         this.bibSourceSelector.applyEntryId(this.selectedBibSource);
564         this.matchSetSelector.applyEntryId(this.selectedMatchSet);
565         this.providerSelector.applyEntryId(this.selectedProvider);
566         this.fiscalYearSelector.applyEntryId(this.selectedFiscalYear);
567         this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile);
568         this.fallThruMergeProfileSelector.applyEntryId(this.selectedFallThruMergeProfile);
569     }
570
571     deleteTemplate() {
572         delete this.formTemplates[this.selectedTemplate];
573         this.formTemplateSelector.selected = null;
574         return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
575     }
576 }
577