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';
24 const TEMPLATE_SETTING_NAME = 'eg.acq.picklist.upload.templates';
26 const TEMPLATE_ATTRS = [
27 'createPurchaseOrder',
28 'activatePurchaseOrder',
39 'selectedMergeProfile',
40 'selectedFallThruMergeProfile',
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'
61 templateUrl: './upload.component.html'
63 export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
65 settings: Object = {};
67 selectedQueue: ComboboxEntry;
70 activeSelectionListId: number;
71 activeQueueId: number;
72 orderingAgency: number;
73 selectedFiscalYear: number;
74 selectedSelectionList: ComboboxEntry;
75 selectedBibSource: number;
76 selectedProvider: number;
77 selectedMatchSet: number;
79 selectedMergeProfile: number;
80 selectedFallThruMergeProfile: number;
84 defaultMatchSet: string;
86 createPurchaseOrder: boolean;
87 activatePurchaseOrder: boolean;
90 importNonMatching: boolean;
91 mergeOnExact: boolean;
92 mergeOnSingleMatch: boolean;
93 mergeOnBestMatch: boolean;
94 minQualityRatio: number;
97 uploadProcessing: boolean;
98 uploadComplete: boolean;
100 // Generated by the server
103 selectedTemplate: string;
104 formTemplates: {[name: string]: any};
105 newTemplateName: string;
107 @ViewChild('fileSelector', { static: false }) private fileSelector;
108 @ViewChild('uploadProgress', { static: true })
109 private uploadProgress: ProgressInlineComponent;
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;
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
140 this.applyDefaults();
141 this.applySettings();
144 applySettings(): Promise<any> {
145 return this.store.getItemBatch(ORG_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']);
162 this.minQualityRatio = 0;
163 this.recordType = 'bib';
164 this.formTemplates = {};
165 if (this.vlagent.importSelection) {
167 if (!this.vlagent.importSelection.queue) {
168 // Incomplete import selection, clear it.
169 this.vlagent.importSelection = null;
173 const queue = this.vlagent.importSelection.queue;
174 this.selectedMatchSet = queue.match_set();
182 this.loadStartupData();
186 this.clearSelection();
189 importSelection(): VandelayImportSelection {
190 return this.vlagent.importSelection;
193 loadStartupData(): Promise<any> {
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']),
210 return Promise.all(promises);
214 orgOnChange(org: IdlObject) {
215 this.orderingAgency = org.id();
219 this.store.getItem(TEMPLATE_SETTING_NAME).then(
221 this.formTemplates = templates || {};
223 Object.keys(this.formTemplates).forEach(name => {
224 if (this.formTemplates[name].default) {
225 this.selectedTemplate = name;
232 formatTemplateEntries(): ComboboxEntry[] {
235 Object.keys(this.formTemplates || {}).forEach(
236 name => entries.push({id: name, label: name}));
241 formatEntries(etype: string): ComboboxEntry[] {
242 const rtype = this.recordType;
247 return (this.vlagent.bibSources || []).map(
249 return {id: s.id(), label: s.source()};
252 case 'providersList':
253 return (this.vlagent.providersList || []).map(
255 return {id: p.id(), label: p.code()};
259 return (this.vlagent.fiscalYears || []).map(
261 return {id: fy.id(), label: fy.year()};
265 case 'selectionLists':
266 list = this.vlagent.selectionLists;
270 list = (this.vlagent.allQueues[rtype] || []);
274 list = this.vlagent.matchSets['bib'];
278 case 'importItemDefs':
279 list = this.vlagent.importItemAttrDefs;
282 case 'mergeProfiles':
283 list = this.vlagent.mergeProfiles;
287 return (list || []).map(item => {
288 return {id: item.id(), label: item.name()};
292 selectEntry($event: ComboboxEntry, etype: string) {
293 const id = $event ? $event.id : null;
297 this.recordType = id;
300 case 'providersList':
301 this.selectedProvider = id;
305 this.selectedBibSource = id;
309 this.selectedFiscalYear = id;
312 case 'selectionLists':
313 this.selectedSelectionList = id;
317 this.selectedMatchSet = id;
321 case 'mergeProfiles':
322 this.selectedMergeProfile = id;
325 case 'FallThruMergeProfile':
326 this.selectedFallThruMergeProfile = id;
331 fileSelected($event) {
332 this.selectedFile = $event.target.files[0];
335 hasNeededData(): boolean {
336 return this.selectedQueue &&
337 Boolean(this.selectedFile) &&
338 Boolean(this.selectedFiscalYear) &&
339 Boolean(this.selectedProvider) &&
340 Boolean(this.orderingAgency);
344 this.sessionKey = null;
345 this.isUploading = true;
346 this.uploadComplete = false;
347 this.resetProgressBars();
349 this.resolveSelectionList(),
353 this.activeQueueId = queueId;
354 return this.uploadFile();
356 err => Promise.reject('queue create failed')
358 ok => this.processUpload(),
359 err => Promise.reject('process spool failed')
362 this.isUploading = false;
363 this.uploadComplete = true;
366 console.log('file upload failed: ', err);
367 this.isUploading = false;
368 this.resetProgressBars();
374 resetProgressBars() {
375 this.uploadProgress.update({value: 0, max: 1});
378 resolveQueue(): Promise<number> {
380 if (this.selectedQueue.freetext) {
381 return this.vlagent.createQueue(
382 this.selectedQueue.label,
385 this.selectedMatchSet,
389 const evt = this.evt.parse(err);
391 if (evt.textcode.match(/QUEUE_EXISTS/)) {
392 this.dupeQueueAlert.open();
394 alert(evt); // server error
398 return Promise.reject('Queue Create Failed');
402 return Promise.resolve(this.selectedQueue.id);
406 resolveSelectionList(): Promise<any> {
407 if (!this.selectedSelectionList) {
408 return Promise.resolve();
410 if (this.selectedSelectionList.id) {
411 this.activeSelectionListId = this.selectedSelectionList.id;
413 if (this.selectedSelectionList.freetext) {
415 return this.vlagent.createSelectionList(
416 this.selectedSelectionList.label,
419 value => this.activeSelectionListId = value
422 return Promise.resolve(this.activeSelectionListId);
425 uploadFile(): Promise<any> {
427 if (this.vlagent.importSelection) {
428 return Promise.resolve();
431 const formData: FormData = new FormData();
433 formData.append('ses', this.auth.token());
434 formData.append('marc_upload',
435 this.selectedFile, this.selectedFile.name);
437 if (this.selectedBibSource) {
438 formData.append('bib_source', '' + this.selectedBibSource);
441 const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
442 {reportProgress: true, responseType: 'text'});
444 return this.http.request(req).pipe(tap(
446 if (evt.type === HttpEventType.UploadProgress) {
447 this.uploadProgress.update(
448 {value: evt.loaded, max: evt.total});
450 } else if (evt instanceof HttpResponse) {
451 this.sessionKey = evt.body as string;
453 'vlagent file uploaded OK with key ' + this.sessionKey);
457 (err: HttpErrorResponse) => {
459 this.toast.danger(err.error);
464 processUpload(): Promise<any> {
466 this.uploadProcessing = true;
468 if (this.vlagent.importSelection) {
469 return Promise.resolve();
472 const spoolType = this.recordType;
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
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
497 const method = `open-ils.acq.process_upload_records`;
499 return new Promise((resolve, reject) => {
501 'open-ils.acq', method,
502 this.auth.token(), this.sessionKey, args
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;
512 if (progress.purchase_order) {this.newPO = progress.purchase_order.id(); }
519 this.vlagent.importSelection = null;
520 this.activeSelectionListId = null;
527 TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
529 console.debug('Saving import profile', template);
531 this.formTemplates[this.selectedTemplate] = template;
532 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
535 markTemplateDefault() {
537 Object.keys(this.formTemplates).forEach(
538 name => delete this.formTemplates.default
541 this.formTemplates[this.selectedTemplate].default = true;
543 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
546 templateSelectorChange(entry: ComboboxEntry) {
549 this.selectedTemplate = '';
553 this.selectedTemplate = entry.label; // label == name
555 if (entry.freetext) {
559 const template = this.formTemplates[entry.id];
561 TEMPLATE_ATTRS.forEach(key => this[key] = template[key]);
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);
572 delete this.formTemplates[this.selectedTemplate];
573 this.formTemplateSelector.selected = null;
574 return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);