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';
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 selector: 'eg-acq-upload',
62 templateUrl: './upload.component.html'
64 export class UploadComponent implements AfterViewInit, OnDestroy {
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';
73 @Input() customAction: (args: any) => void;
74 customActionProcessing = false;
76 settings: Object = {};
78 selectedQueue: ComboboxEntry;
81 activeSelectionListId: number;
82 activeQueueId: number;
83 orderingAgency: number;
84 selectedFiscalYear: number;
85 selectedSelectionList: ComboboxEntry;
86 selectedBibSource: number;
87 selectedProvider: number;
88 selectedMatchSet: number;
90 selectedMergeProfile: number;
91 selectedFallThruMergeProfile: number;
95 defaultMatchSet: string;
97 createPurchaseOrder: boolean;
98 activatePurchaseOrder: boolean;
101 importNonMatching: boolean;
102 mergeOnExact: boolean;
103 mergeOnSingleMatch: boolean;
104 mergeOnBestMatch: boolean;
105 minQualityRatio: number;
107 isUploading: boolean;
108 uploadProcessing: boolean;
109 uploadError: boolean;
110 uploadErrorCode: string;
111 uploadErrorText: string;
112 uploadComplete: boolean;
114 // Generated by the server
117 selectedTemplate: string;
118 formTemplates: {[name: string]: any};
119 newTemplateName: string;
121 @ViewChild('fileSelector', { static: false }) private fileSelector;
122 @ViewChild('uploadProgress', { static: true })
123 private uploadProgress: ProgressInlineComponent;
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;
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
162 // force a reload of the component if we navigate to it
164 this.router.routeReuseStrategy.shouldReuseRoute = () => {
167 this.applyDefaults();
168 this.applySettings();
171 applySettings(): Promise<any> {
172 return this.store.getItemBatch(ORG_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']);
189 this.minQualityRatio = 0;
190 this.recordType = 'bib';
191 this.formTemplates = {};
192 if (this.vlagent.importSelection) {
194 if (!this.vlagent.importSelection.queue) {
195 // Incomplete import selection, clear it.
196 this.vlagent.importSelection = null;
200 const queue = this.vlagent.importSelection.queue;
201 this.selectedMatchSet = queue.match_set();
207 this.loadStartupData();
211 this.clearSelection();
214 importSelection(): VandelayImportSelection {
215 return this.vlagent.importSelection;
218 loadStartupData(): Promise<any> {
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);
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']),
241 return Promise.all(promises);
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); }
255 this.store.getItem(TEMPLATE_SETTING_NAME).then(
257 this.formTemplates = templates || {};
259 Object.keys(this.formTemplates).forEach(name => {
260 if (this.formTemplates[name].default) {
261 this.selectedTemplate = name;
268 formatTemplateEntries(): ComboboxEntry[] {
271 Object.keys(this.formTemplates || {}).forEach(
272 name => entries.push({id: name, label: name}));
277 formatEntries(etype: string): ComboboxEntry[] {
278 const rtype = this.recordType;
283 return (this.vlagent.bibSources || []).map(
285 return {id: s.id(), label: s.source()};
289 return (this.vlagent.fiscalYears || []).map(
291 return {id: fy.id(), label: fy.year()};
295 case 'selectionLists':
296 list = this.vlagent.selectionLists;
300 list = (this.vlagent.allQueues[rtype] || []);
304 list = this.vlagent.matchSets['bib'];
308 case 'importItemDefs':
309 list = this.vlagent.importItemAttrDefs;
312 case 'mergeProfiles':
313 list = this.vlagent.mergeProfiles;
317 return (list || []).map(item => {
318 return {id: item.id(), label: item.name()};
322 selectEntry($event: ComboboxEntry, etype: string) {
323 const id = $event ? $event.id : null;
327 this.recordType = id;
331 this.selectedBibSource = id;
335 this.selectedFiscalYear = id;
338 case 'selectionLists':
339 this.selectedSelectionList = id;
343 this.selectedMatchSet = id;
347 case 'mergeProfiles':
348 this.selectedMergeProfile = id;
351 case 'FallThruMergeProfile':
352 this.selectedFallThruMergeProfile = id;
357 fileSelected($event) {
358 this.selectedFile = $event.target.files[0];
361 hasNeededData(): boolean {
362 if (this.mode === 'getImportParams') {
363 return this.selectedQueue ? true : false;
365 return this.selectedQueue &&
366 Boolean(this.selectedFile) &&
367 Boolean(this.selectedFiscalYear) &&
368 Boolean(this.selectedProvider) &&
369 Boolean(this.orderingAgency);
373 this.sessionKey = null;
374 this.isUploading = true;
375 this.uploadComplete = false;
376 this.resetProgressBars();
378 this.resolveSelectionList(),
382 this.activeQueueId = queueId;
383 return this.uploadFile();
385 err => Promise.reject('queue create failed')
387 ok => this.processUpload(),
388 err => Promise.reject('process spool failed')
391 this.isUploading = false;
392 this.uploadComplete = true;
395 console.log('file upload failed: ', err);
396 this.isUploading = false;
397 this.resetProgressBars();
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() : '';
414 performCustomAction() {
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
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
440 this.customActionProcessing = true;
441 this.customAction(args);
444 resetProgressBars() {
445 this.uploadProgress.update({value: 0, max: 1});
448 resolveQueue(): Promise<number> {
450 if (this.selectedQueue.freetext) {
451 return this.vlagent.createQueue(
452 this.selectedQueue.label,
455 this.selectedMatchSet,
459 const evt = this.evt.parse(err);
461 if (evt.textcode.match(/QUEUE_EXISTS/)) {
462 this.dupeQueueAlert.open();
464 alert(evt); // server error
468 return Promise.reject('Queue Create Failed');
472 return Promise.resolve(this.selectedQueue.id);
476 resolveSelectionList(): Promise<any> {
477 if (!this.selectedSelectionList) {
478 return Promise.resolve();
480 if (this.selectedSelectionList.id) {
481 this.activeSelectionListId = this.selectedSelectionList.id;
483 if (this.selectedSelectionList.freetext) {
485 return this.vlagent.createSelectionList(
486 this.selectedSelectionList.label,
489 value => this.activeSelectionListId = value
492 return Promise.resolve(this.activeSelectionListId);
495 uploadFile(): Promise<any> {
497 if (this.vlagent.importSelection) {
498 return Promise.resolve();
501 const formData: FormData = new FormData();
503 formData.append('ses', this.auth.token());
504 formData.append('marc_upload',
505 this.selectedFile, this.selectedFile.name);
507 if (this.selectedBibSource) {
508 formData.append('bib_source', '' + this.selectedBibSource);
511 const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
512 {reportProgress: true, responseType: 'text'});
514 return this.http.request(req).pipe(tap(
516 if (evt.type === HttpEventType.UploadProgress) {
517 this.uploadProgress.update(
518 {value: evt.loaded, max: evt.total});
520 } else if (evt instanceof HttpResponse) {
521 this.sessionKey = evt.body as string;
523 'vlagent file uploaded OK with key ' + this.sessionKey);
527 (err: HttpErrorResponse) => {
529 this.toast.danger(err.error);
534 processUpload(): Promise<any> {
536 this.uploadProcessing = true;
537 this.uploadError = false;
539 if (this.vlagent.importSelection) {
540 return Promise.resolve();
543 const spoolType = this.recordType;
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
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
569 const method = `open-ils.acq.process_upload_records`;
571 return new Promise((resolve, reject) => {
573 'open-ils.acq', method,
574 this.auth.token(), this.sessionKey, args
577 const resp = this.evt.parse(progress);
578 console.log(progress);
580 this.uploadError = true;
581 this.uploadErrorCode = resp.textcode;
582 this.uploadErrorText = resp.payload;
583 this.uploadProcessing = false;
584 this.uploadComplete = true;
587 if (progress.complete) {
588 this.uploadProcessing = false;
589 this.uploadComplete = true;
591 if (progress.purchase_order) {this.newPO = progress.purchase_order.id(); }
598 this.vlagent.importSelection = null;
599 this.activeSelectionListId = null;
606 TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
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))
615 markTemplateDefault() {
617 Object.keys(this.formTemplates).forEach(
618 name => delete this.formTemplates[name].default
621 this.formTemplates[this.selectedTemplate].default = true;
623 this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates).then(x =>
624 this.loadMarcOrderTemplateSetAsDefaultString.current()
625 .then(str => this.toast.success(str))
629 templateSelectorChange(entry: ComboboxEntry) {
632 this.selectedTemplate = '';
636 this.selectedTemplate = entry.label; // label == name
638 if (entry.freetext) {
642 const template = this.formTemplates[entry.id];
644 TEMPLATE_ATTRS.forEach(key => this[key] = template[key]);
646 this.bibSourceSelector.applyEntryId(this.selectedBibSource);
647 this.matchSetSelector.applyEntryId(this.selectedMatchSet);
648 if (this.providerSelector) {
649 this.providerSelector.selectedId = this.selectedProvider;
651 if (this.fiscalYearSelector) {
652 this.fiscalYearSelector.applyEntryId(this.selectedFiscalYear);
654 this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile);
655 this.fallThruMergeProfileSelector.applyEntryId(this.selectedFallThruMergeProfile);
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))