import {PicklistRoutingModule} from './routing.module';
import {PicklistComponent} from './picklist.component';
import {PicklistSummaryComponent} from './summary.component';
+import {HttpClientModule} from '@angular/common/http';
+import {UploadComponent} from './upload.component';
@NgModule({
declarations: [
PicklistComponent,
- PicklistSummaryComponent
+ PicklistSummaryComponent,
+ UploadComponent
],
imports: [
StaffCommonModule,
CatalogCommonModule,
LineitemModule,
HoldingsModule,
- PicklistRoutingModule
+ PicklistRoutingModule,
+ HttpClientModule
],
providers: []
})
import {LineitemWorksheetComponent} from '../lineitem/worksheet.component';
import {BriefRecordComponent} from '../lineitem/brief-record.component';
import {LineitemHistoryComponent} from '../lineitem/history.component';
+import {UploadComponent} from './upload.component';
+import {VandelayService} from '@eg/staff/cat/vandelay/vandelay.service';
+import {PicklistUploadService} from './upload.service'
const routes: Routes = [{
path: 'brief-record',
component: BriefRecordComponent
}, {
+ path: 'upload',
+ component: UploadComponent
+}, {
path: ':picklistId',
component: PicklistComponent,
children : [{
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
- providers: []
+ providers: [VandelayService, PicklistUploadService]
})
export class PicklistRoutingModule {}
--- /dev/null
+<eg-staff-banner bannerText="Load MARC Order Records" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="row">
+ <div class="ml-auto mr-3"><a i18n href="/eg/staff/acq/legacy/picklist/upload">Legacy Upload Interface</a></div>
+</div>
+
+<div class="row mb-3" *ngIf="importSelection()">
+ <div class="col-lg-2" *ngIf="selectedQueue">
+ <button class="btn btn-info label-with-material-icon"
+ routerLink="/staff/cat/vandelay/queue/{{recordType}}/{{selectedQueue.id}}">
+ <span class="material-icons">arrow_back</span>
+ <span i18n>Return to Queue</span>
+ </button>
+ </div>
+</div>
+
+<eg-alert-dialog #dupeQueueAlert i18n-dialogBody
+ dialogBody="A queue with the requested name already exists.">
+</eg-alert-dialog>
+
+<div class="common-form striped-odd form-validated ml-3 mr-3">
+ <div class="row">
+ <div class="col-lg-3">
+ <label for="template-select" i18n>Apply/Create Form Template</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox #formTemplateSelector
+ id="template-select"
+ (onChange)="templateSelectorChange($event)"
+ [allowFreeText]="true"
+ [startId]="selectedTemplate"
+ [startIdFiresOnChange]="true"
+ [entries]="formatTemplateEntries()">
+ </eg-combobox>
+ </div>
+ <div class="col-lg-6">
+ <button class="btn btn-success"
+ [disabled]="!selectedTemplate"
+ (click)="saveTemplate()" i18n>Save As New Template</button>
+ <button class="btn btn-outline-primary ml-3"
+ [disabled]="!selectedTemplate"
+ (click)="markTemplateDefault()" i18n>Mark Template as Default</button>
+ <button class="btn btn-danger ml-3"
+ [disabled]="!selectedTemplate"
+ (click)="deleteTemplate()" i18n>Delete Template</button>
+ </div>
+ </div>
+
+ <h2>Purchase Order</h2>
+ <div class="row">
+ <div class="col-lg-3">
+ <label for="provider-select" i18n>Provider</label>
+ </div>
+
+ <div class="col-lg-3">
+ <eg-combobox #providerSelector
+ id="provider-select"
+ [entries]="formatEntries('providersList')"
+ (onChange)="selectEntry($event, 'providersList')"
+ [required]="true"
+ [startId]="selectedProvider">
+ </eg-combobox>
+ </div>
+
+ <div class="col-lg-3">
+ <label for="create-po" i18n>Create Purchase Order</label>
+ </div>
+ <div class="col-lg-3">
+ <input class="form-check-input" type="checkbox"
+ id="create-po"
+ [(ngModel)]="createPurchaseOrder">
+ </div>
+</div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label for="type-select" i18n>Ordering Agency</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-org-select
+ [initialOrgId]="1"
+ [applyOrgId]="orderingAgency"
+ (onChange)="orgOnChange($event)">
+ </eg-org-select>
+ </div>
+
+ <div class="col-lg-3">
+ <label for="activate-po" i18n>Activate Purchase Order</label>
+ </div>
+
+ <div class="col-lg-3">
+ <input class="form-check-input" type="checkbox"
+ id="activate-po"
+ [(ngModel)]="activatePurchaseOrder">
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-lg-3">
+ <label for="year-select" i18n>Fiscal Year</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox #fiscalYearSelector
+ id="year-select"
+ [startId]="selectedFiscalYear"
+ [entries]="formatEntries('fiscalYears')"
+ (onChange)="selectEntry($event, 'fiscalYears')">
+ </eg-combobox>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-lg-3">
+ <label for="sl-select" i18n>Add to Selection List?</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox #selectionListSelector
+ id="sl-select"
+ [startId]="selectedSelectionList"
+ [entries]="formatEntries('selectionLists')"
+ (onChange)="selectEntry($event, 'selectionLists')">
+ </eg-combobox>
+ </div>
+ </div>
+
+
+ <h2>Upload Settings</h2>
+
+ <div class="row">
+ <div class="col-lg-3">
+ <label for="source-select" i18n>Select a Record Source</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox #bibSourceSelector
+ id="source-select"
+ [entries]="formatEntries('bibSources')"
+ (onChange)="selectEntry($event, 'bibSources')"
+ [required]="true"
+ [startId]="selectedBibSource">
+ </eg-combobox>
+ </div>
+ <div class="col-lg-3">
+ <label for="import-non-matching" i18n>Import Non-Matching Records</label>
+ </div>
+ <div class="col-lg-3">
+ <input class="form-check-input" type="checkbox"
+ [required]="true"
+ id="import-non-matching"
+ [(ngModel)]="importNonMatching">
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-lg-3">
+ <label for="match-set-select" i18n>Record Match Set</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox #matchSetSelector
+ id="match-set-select"
+ [entries]="formatEntries('matchSets')"
+ [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
+ [required]="true"
+ [startId]="selectedMatchSet || defaultMatchSet"
+ (onChange)="selectEntry($event, 'matchSets')">
+ </eg-combobox>
+ </div>
+ <div class="col-lg-3">
+ <label for="merge-on-exact" i18n>Merge On Exact Match (901c)</label>
+ </div>
+ <div class="col-lg-3">
+ <input class="form-check-input" type="checkbox"
+ id="merge-on-exact" [(ngModel)]="mergeOnExact">
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-lg-3">
+ <label for="merge-profiles" i18n>Merge Profile</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox #mergeProfileSelector
+ id="merge-profiles"
+ [required]="true"
+ [entries]="formatEntries('mergeProfiles')"
+ (onChange)="selectEntry($event, 'mergeProfiles')"
+ [startId]="selectedMergeProfile">
+ </eg-combobox>
+ </div>
+ <div class="col-lg-3">
+ <label for="merge-on-single" i18n>Merge On Single Match</label>
+ </div>
+ <div class="col-lg-3">
+ <input class="form-check-input" type="checkbox"
+ id="merge-on-single" [(ngModel)]="mergeOnSingleMatch">
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label for="insuff-merge-profiles" i18n>
+ Insufficient Quality Fall-Through Profile
+ </label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox #fallThruMergeProfileSelector
+ id="insuff-merge-profiles"
+ [entries]="formatEntries('mergeProfiles')"
+ (onChange)="selectEntry($event, 'FallThruMergeProfile')"
+ [startId]="selectedFallThruMergeProfile">
+ </eg-combobox>
+ </div>
+ <div class="col-lg-3">
+ <label for="merge-on-best" i18n>Merge On Best Match</label>
+ </div>
+ <div class="col-lg-3">
+ <input class="form-check-input" type="checkbox"
+ id="merge-on-best" [(ngModel)]="mergeOnBestMatch">
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label for="min-quality-ratio" i18n>
+ Best/Single Match Minimum Quality Ratio
+ </label>
+ </div>
+ <div class="col-lg-3">
+ <input type="number" step="0.1" id="min-quality-ratio"
+ class="form-control" [(ngModel)]="minQualityRatio">
+ </div>
+ <div class="col-lg-3">
+ <label for="load-items" i18n>Load Items for Imported Records</label>
+ </div>
+ <div class="col-lg-3">
+ <input class="form-check-input" type="checkbox"
+ id="load-items"
+ [(ngModel)]="loadItems">
+ </div>
+ </div>
+
+ <h2>This Upload</h2>
+ <div class="row">
+ <div class="col-lg-3">
+ <label for="queue-select" i18n>Select or Create a Queue</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox [entries]="formatEntries('activeQueues')"
+ id="queue-select"
+ [startId]="startQueueId"
+ [startIdFiresOnChange]="true"
+ [disabled]="startQueueId"
+ (onChange)="selectedQueue=$event" i18n-placeholder
+ [required]="true"
+ [allowFreeText]="true">
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="row" *ngIf="!importSelection()">
+ <div class="col-lg-3">
+ <label for="upload-file" i18n>File to Upload:</label>
+ </div>
+ <div class="col-lg-3">
+ <input #fileSelector (change)="fileSelected($event)"
+ id="upload-file" required class="form-control" type="file"/>
+ </div>
+ </div>
+ <div class="row" *ngIf="importSelection()">
+ <div class="col-lg-3">
+ <label>Import Selected</label>
+ </div>
+ <div class="col-lg-3">
+ <span *ngIf="!importSelection().importQueue" i18n>
+ Importing {{importSelection().recordIds.length}} Record(s)</span>
+ <span *ngIf="importSelection().importQueue" i18n>
+ Importing Queue {{importSelection().queue.name()}}</span>
+ </div>
+ <div class="col-lg-3">
+ <button class="btn btn-outline-info ml-2" (click)="clearSelection()" i18n>
+ Clear Selection
+ </button>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-6 offset-lg-3">
+ <button class="btn btn-success btn-lg btn-block font-weight-bold"
+ [disabled]="isUploading || !hasNeededData()"
+ (click)="upload()" i18n>Upload</button>
+ </div>
+ </div>
+ <div class="row" [hidden]="!showProgress || importSelection()">
+ <div class="col-lg-3">
+ <label i18n>Upload to Server</label>
+ </div>
+ <div class="col-lg-6">
+ <eg-progress-inline #uploadProgress></eg-progress-inline>
+ </div>
+ </div>
+
+ <div class="col-lg-6" [hidden]="!isUploading">
+ <label i18n>Go To:</label>
+ </div>
+ <div class="row" [hidden]="!isUploading">
+ <div class="col-lg-6 offset-lg-3">
+ <a routerLink="/staff/cat/vandelay/queue/{{recordType}}/{{activeQueueId}}" target="_blank" i18n>Queue</a>
+ </div>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+import {Component, OnInit, AfterViewInit, Input,
+ ViewChild, OnDestroy} from '@angular/core';
+import {Subject} from 'rxjs';
+import {tap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ComboboxComponent,
+ ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {VandelayImportSelection,
+ VANDELAY_UPLOAD_PATH} from '@eg/staff/cat/vandelay/vandelay.service';
+import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
+import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
+import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {PicklistUploadService} from './upload.service';
+import {OrgSelectComponent} from '@eg/share/org-select/org-select.component'
+
+
+const TEMPLATE_SETTING_NAME = 'eg.acq.picklist.upload.templates';
+
+const TEMPLATE_ATTRS = [
+ 'createPurchaseOrder',
+ 'activatePurchaseOrder',
+ 'orderingAgency',
+ 'selectedFiscalYear',
+ 'loadItems',
+ 'selectedBibSource',
+ 'selectedMatchSet',
+ 'mergeOnExact',
+ 'importNonMatching',
+ 'mergeOnBestMatch',
+ 'mergeOnSingleMatch',
+ 'selectedMergeProfile',
+ 'selectedFallThruMergeProfile',
+ 'minQualityRatio'
+];
+
+interface ImportOptions {
+ session_key: string;
+ overlay_map?: {[qrId: number]: /* breId */ number};
+ // import_no_match?: boolean;
+ // auto_overlay_exact?: boolean;
+ // auto_overlay_best_match?: boolean;
+ // auto_overlay_1match?: boolean;
+ // merge_profile?: any;
+ // fall_through_merge_profile?: any;
+// match_quality_ratio: number;
+ // match_set: number;
+ // bib_source: number;
+ exit_early: boolean;
+}
+
+@Component({
+ templateUrl: './upload.component.html'
+})
+export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
+
+ recordType: string;
+ selectedQueue: ComboboxEntry; // freetext enabled
+
+ // used for applying a default queue ID value when we have
+ // a load-time queue before the queue combobox entries exist.
+ startQueueId: number;
+
+ activeQueueId: number;
+ orderingAgency: number;
+ selectedFiscalYear: number;
+ selectedSelectionList: number;
+ selectedBibSource: number;
+ selectedProvider: number;
+ selectedMatchSet: number;
+ importDefId: number;
+ selectedMergeProfile: number;
+ selectedFallThruMergeProfile: number;
+ selectedFile: File;
+
+ defaultMatchSet: string;
+
+ createPurchaseOrder: boolean;
+ activatePurchaseOrder: boolean;
+ loadItems: boolean;
+
+ importNonMatching: boolean;
+ mergeOnExact: boolean;
+ mergeOnSingleMatch: boolean;
+ mergeOnBestMatch: boolean;
+ minQualityRatio: number;
+
+ // True after the first upload, then remains true.
+ showProgress: boolean;
+
+ // Upload in progress.
+ isUploading: boolean;
+
+ // True only after successful upload
+ uploadComplete: boolean;
+
+ // Upload / processsing session key
+ // Generated by the server
+ sessionKey: string;
+
+
+ selectedTemplate: string;
+ formTemplates: {[name: string]: any};
+ newTemplateName: string;
+
+ @ViewChild('fileSelector', { static: false }) private fileSelector;
+ @ViewChild('uploadProgress', { static: true })
+ private uploadProgress: ProgressInlineComponent;
+
+ // Need these refs so values can be applied via external stimuli
+ @ViewChild('formTemplateSelector', { static: true })
+ private formTemplateSelector: ComboboxComponent;
+ @ViewChild('bibSourceSelector', { static: true })
+ private bibSourceSelector: ComboboxComponent;
+ @ViewChild('providerSelector', {static: true})
+ private providerSelector: ComboboxComponent;
+ @ViewChild('fiscalYearSelector', { static: true })
+ private fiscalYearSelector: ComboboxComponent;
+ @ViewChild('selectionListSelector', { static: true })
+ private selectionListSelector: ComboboxComponent;
+ @ViewChild('matchSetSelector', { static: true })
+ private matchSetSelector: ComboboxComponent;
+ @ViewChild('mergeProfileSelector', { static: true })
+ private mergeProfileSelector: ComboboxComponent;
+ @ViewChild('fallThruMergeProfileSelector', { static: true })
+ private fallThruMergeProfileSelector: ComboboxComponent;
+ @ViewChild('dupeQueueAlert', { static: true })
+ private dupeQueueAlert: AlertDialogComponent;
+
+ constructor(
+ private http: HttpClient,
+ private toast: ToastService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private org: OrgService,
+ private store: ServerStoreService,
+ private vlagent: PicklistUploadService
+ ) {
+ this.applyDefaults();
+ }
+
+ applyDefaults() {
+ this.minQualityRatio = 0;
+ this.selectedBibSource = 1; // default to system local
+ this.recordType = 'bib';
+ this.formTemplates = {};
+//To-do add default for fiscal year
+ if (this.vlagent.importSelection) {
+
+ if (!this.vlagent.importSelection.queue) {
+ // Incomplete import selection, clear it.
+ this.vlagent.importSelection = null;
+ return;
+ }
+
+ const queue = this.vlagent.importSelection.queue;
+ this.recordType = queue.queue_type();
+ this.selectedMatchSet = queue.match_set();
+
+ // This will be propagated to selectedQueue as a combobox
+ // entry via the combobox
+ this.startQueueId = queue.id();
+
+
+ }
+ }
+
+ ngOnInit() {}
+
+ ngAfterViewInit() {
+ this.loadStartupData();
+ }
+
+ ngOnDestroy() {
+ // Always clear the import selection when navigating away from
+ // the import page.
+ this.clearSelection();
+ }
+
+ importSelection(): VandelayImportSelection {
+ return this.vlagent.importSelection;
+ }
+
+ loadStartupData(): Promise<any> {
+
+
+ const promises = [
+ this.vlagent.getMergeProfiles(),
+ this.vlagent.getAllQueues('bib'),
+ this.vlagent.getMatchSets('bib'),
+ this.vlagent.getBibSources(),
+ this.vlagent.getFiscalYears(),
+ this.vlagent.getProvidersList(),
+ this.vlagent.getSelectionLists(),
+ this.vlagent.getItemImportDefs(),
+ this.org.settings(['vandelay.default_match_set']).then(
+ s => this.defaultMatchSet = s['vandelay.default_match_set']),
+ this.loadTemplates()
+ ];
+
+ return Promise.all(promises);
+ }
+
+ orgOnChange(org: IdlObject) {
+ this.orderingAgency = org.id()
+ }
+
+ loadTemplates() {
+ this.store.getItem(TEMPLATE_SETTING_NAME).then(
+ templates => {
+ this.formTemplates = templates || {};
+
+ Object.keys(this.formTemplates).forEach(name => {
+ if (this.formTemplates[name].default) {
+ this.selectedTemplate = name;
+ }
+ });
+ }
+ );
+ }
+
+ formatTemplateEntries(): ComboboxEntry[] {
+ const entries = [];
+
+ Object.keys(this.formTemplates || {}).forEach(
+ name => entries.push({id: name, label: name}));
+
+ return entries;
+ }
+
+ // Format typeahead data sets
+ formatEntries(etype: string): ComboboxEntry[] {
+ const rtype = this.recordType;
+ let list;
+
+ switch (etype) {
+ case 'bibSources':
+ return (this.vlagent.bibSources || []).map(
+ s => {
+ return {id: s.id(), label: s.source()};
+ });
+
+ case 'providersList':
+ return (this.vlagent.providersList || []).map(
+ p => {
+ return {id: p.id(), label: p.code()};
+ });
+
+ case 'fiscalYears':
+ return (this.vlagent.fiscalYears || []).map(
+ fy => {
+ return {id: fy.id(), label: fy.year()};
+ });
+ break;
+
+ case 'selectionLists':
+ list = this.vlagent.selectionLists;
+ break;
+
+ case 'activeQueues':
+ list = (this.vlagent.allQueues[rtype] || [])
+ .filter(q => q.complete() === 'f');
+ break;
+
+ case 'matchSets':
+ list = this.vlagent.matchSets['bib'];
+ break;
+
+
+ case 'importItemDefs':
+ list = this.vlagent.importItemAttrDefs;
+ break;
+
+ case 'mergeProfiles':
+ list = this.vlagent.mergeProfiles;
+ break;
+ }
+
+ return (list || []).map(item => {
+ return {id: item.id(), label: item.name()};
+ });
+ }
+
+ selectEntry($event: ComboboxEntry, etype: string) {
+ const id = $event ? $event.id : null;
+
+ switch (etype) {
+ case 'recordType':
+ this.recordType = id;
+ break;
+
+ case 'providersList':
+ this.selectedProvider = id;
+ break;
+
+ case 'bibSources':
+ this.selectedBibSource = id;
+ break;
+
+ case 'fiscalYears':
+ this.selectedFiscalYear = id;
+ break;
+
+ case 'selectionLists':
+ this.selectedSelectionList = id;
+ break;
+
+ case 'matchSets':
+ this.selectedMatchSet = id;
+ break;
+
+
+ case 'mergeProfiles':
+ this.selectedMergeProfile = id;
+ break;
+
+ case 'FallThruMergeProfile':
+ this.selectedFallThruMergeProfile = id;
+ break;
+ }
+ }
+
+ fileSelected($event) {
+ this.selectedFile = $event.target.files[0];
+ }
+
+ // Required form data varies depending on context.
+ hasNeededData(): boolean {
+ if (this.vlagent.importSelection) {
+ return this.importActionSelected();
+ } else {
+ return this.selectedQueue &&
+ Boolean(this.recordType) && Boolean(this.selectedFile);
+ }
+ }
+
+ importActionSelected(): boolean {
+ return this.importNonMatching
+ || this.mergeOnExact
+ || this.mergeOnSingleMatch
+ || this.mergeOnBestMatch;
+ }
+
+ // 1. create queue if necessary
+ // 2. upload MARC file
+ // 3. Enqueue MARC records
+ // 4. Import records
+ upload() {
+ this.sessionKey = null;
+ this.showProgress = true;
+ this.isUploading = true;
+ this.uploadComplete = false;
+ this.resetProgressBars();
+
+ this.resolveQueue()
+ .then(
+ queueId => {
+ this.activeQueueId = queueId;
+ return this.uploadFile();
+ },
+ err => Promise.reject('queue create failed')
+ ).then(
+ ok => this.processUpload(),
+ err => Promise.reject('process spool failed')
+ ).then(
+ ok => {
+ this.isUploading = false;
+ this.uploadComplete = true;
+ },
+ err => {
+ console.log('file upload failed: ', err);
+ this.isUploading = false;
+ this.resetProgressBars();
+
+ }
+ );
+ }
+
+ resetProgressBars() {
+ this.uploadProgress.update({value: 0, max: 1});
+ }
+
+ // Extract selected queue ID or create a new queue when requested.
+ resolveQueue(): Promise<number> {
+
+ if (this.selectedQueue.freetext) {
+ // Free text queue selector means create a new entry.
+ // TODO: first check for name dupes
+
+ return this.vlagent.createQueue(
+ this.selectedQueue.label,
+ this.recordType,
+ this.importDefId,
+ this.selectedMatchSet,
+ ).then(
+ id => id,
+ err => {
+ const evt = this.evt.parse(err);
+ if (evt) {
+ if (evt.textcode.match(/QUEUE_EXISTS/)) {
+ this.dupeQueueAlert.open();
+ } else {
+ alert(evt); // server error
+ }
+ }
+
+ return Promise.reject('Queue Create Failed');
+ }
+ );
+ } else {
+ return Promise.resolve(this.selectedQueue.id);
+ }
+ }
+
+ uploadFile(): Promise<any> {
+
+ if (this.vlagent.importSelection) {
+ // Nothing to upload when processing pre-queued records.
+ return Promise.resolve();
+ }
+
+ const formData: FormData = new FormData();
+
+ formData.append('ses', this.auth.token());
+ formData.append('marc_upload',
+ this.selectedFile, this.selectedFile.name);
+
+ if (this.selectedBibSource) {
+ formData.append('bib_source', '' + this.selectedBibSource);
+ }
+
+ const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
+ {reportProgress: true, responseType: 'text'});
+
+ return this.http.request(req).pipe(tap(
+ evt => {
+ if (evt.type === HttpEventType.UploadProgress) {
+ this.uploadProgress.update(
+ {value: evt.loaded, max: evt.total});
+
+ } else if (evt instanceof HttpResponse) {
+ this.sessionKey = evt.body as string;
+ console.log(
+ 'vlagent file uploaded OK with key ' + this.sessionKey);
+ }
+ },
+
+ (err: HttpErrorResponse) => {
+ console.error(err);
+ this.toast.danger(err.error);
+ }
+ )).toPromise();
+ }
+
+ processUpload(): Promise<any> {
+
+ if (this.vlagent.importSelection) {
+ return Promise.resolve();
+ }
+
+ let spoolType = this.recordType;
+
+ const vandelayOptions = {
+ import_no_match: this.importNonMatching,
+ auto_overlay_exact: this.mergeOnExact,
+ auto_overlay_best_match: this.mergeOnBestMatch,
+ auto_overlay_1match: this.mergeOnSingleMatch,
+ merge_profile: this.selectedMergeProfile,
+ fall_through_merge_profile: this.selectedFallThruMergeProfile,
+ match_quality_ratio: this.minQualityRatio,
+ bib_source: this.selectedBibSource,
+ create_assets: this.loadItems,
+ queue_name: this.selectedQueue.label
+
+ }
+
+
+ const args = {
+
+ provider: this.selectedProvider,
+ ordering_agency: this.orderingAgency,
+ create_po: this.createPurchaseOrder,
+ activate_po: this.activatePurchaseOrder,
+ fiscal_year: this.selectedFiscalYear,
+ picklist: this.selectedSelectionList,
+ vandelay: vandelayOptions
+ }
+
+
+ const method = `open-ils.acq.process_upload_records`;
+
+ return new Promise((resolve, reject) => {
+ this.net.request(
+ 'open-ils.acq', method,
+ this.auth.token(), this.sessionKey, args
+ ).subscribe(
+ tracker => {
+ const e = this.evt.parse(tracker);
+ if (e) { console.error(e); return reject(); }
+ }
+ );
+ });
+ }
+
+ clearSelection() {
+ this.vlagent.importSelection = null;
+ this.startQueueId = null;
+ }
+
+ openQueue() {
+ console.log('opening queue ' + this.activeQueueId);
+ }
+
+ saveTemplate() {
+
+ const template = {};
+ TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
+
+ console.debug('Saving import profile', template);
+
+ this.formTemplates[this.selectedTemplate] = template;
+ return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
+ }
+
+ markTemplateDefault() {
+
+ Object.keys(this.formTemplates).forEach(
+ name => delete this.formTemplates.default
+ );
+
+ this.formTemplates[this.selectedTemplate].default = true;
+
+ return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
+ }
+
+ templateSelectorChange(entry: ComboboxEntry) {
+
+ if (!entry) {
+ this.selectedTemplate = '';
+ return;
+ }
+
+ this.selectedTemplate = entry.label; // label == name
+
+ if (entry.freetext) {
+ // User is entering a new template name.
+ // Nothing to apply.
+ return;
+ }
+
+ // User selected an existing template, apply it to the form.
+
+ const template = this.formTemplates[entry.id];
+
+ // Copy the template values into "this"
+ TEMPLATE_ATTRS.forEach(key => this[key] = template[key]);
+
+ // Some values must be manually passed to the combobox'es
+
+ this.bibSourceSelector.applyEntryId(this.selectedBibSource);
+ this.matchSetSelector.applyEntryId(this.selectedMatchSet);
+ this.providerSelector.applyEntryId(this.selectedProvider);
+ this.fiscalYearSelector.applyEntryId(this.selectedFiscalYear);
+ this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile);
+ this.fallThruMergeProfileSelector
+ .applyEntryId(this.selectedFallThruMergeProfile);
+ }
+
+ deleteTemplate() {
+ delete this.formTemplates[this.selectedTemplate];
+ this.formTemplateSelector.selected = null;
+ return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
+ }
+}
+
--- /dev/null
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs';
+import {tap, map} from 'rxjs/operators';
+import {HttpClient} from '@angular/common/http';
+import {saveAs} from 'file-saver';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {PermService} from '@eg/core/perm.service';
+import {EventService} from '@eg/core/event.service';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {VandelayImportSelection, VANDELAY_EXPORT_PATH} from '@eg/staff/cat/vandelay/vandelay.service'
+
+
+@Injectable()
+export class PicklistUploadService {
+
+ allQueues: {[qtype: string]: IdlObject[]};
+ attrDefs: {[atype: string]: IdlObject[]};
+ bibSources: IdlObject[];
+ matchSets: {[stype: string]: IdlObject[]};
+ importItemAttrDefs: IdlObject[];
+ mergeProfiles: IdlObject[];
+ providersList: IdlObject[];
+ fiscalYears: IdlObject[];
+ selectionLists: IdlObject[];
+
+ // Used for tracking records between the queue page and
+ // the import page. Fields managed externally.
+ importSelection: VandelayImportSelection;
+
+ // Track the last grid offset in the queue page so we
+ // can return the user to the same page of data after
+ // going to the matches page.
+ queuePageOffset: number;
+
+ constructor(
+ private http: HttpClient,
+ private idl: IdlService,
+ private org: OrgService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private perm: PermService
+ ) {
+ this.attrDefs = {};
+ this.allQueues = {};
+ this.matchSets = {};
+ this.importSelection = null;
+ this.queuePageOffset = 0;
+ }
+
+ getAttrDefs(dtype: string): Promise<IdlObject[]> {
+ if (this.attrDefs[dtype]) {
+ return Promise.resolve(this.attrDefs[dtype]);
+ }
+ const cls = (dtype === 'bib') ? 'vqbrad' : 'vqarad';
+ const orderBy = {};
+ orderBy[cls] = 'id';
+ return this.pcrud.retrieveAll(cls,
+ {order_by: orderBy}, {atomic: true}).toPromise()
+ .then(list => {
+ this.attrDefs[dtype] = list;
+ return list;
+ });
+ }
+
+ getMergeProfiles(): Promise<IdlObject[]> {
+ if (this.mergeProfiles) {
+ return Promise.resolve(this.mergeProfiles);
+ }
+
+ const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+ return this.pcrud.search('vmp',
+ {owner: owners}, {order_by: {vmp: ['name']}}, {atomic: true})
+ .toPromise().then(profiles => {
+ this.mergeProfiles = profiles;
+ return profiles;
+ });
+ }
+
+ getProvidersList(): Promise<IdlObject[]> {
+ if (this.providersList) {
+ return Promise.resolve(this.providersList);
+ }
+
+ const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+ return this.pcrud.search('acqpro',
+ {owner: owners}, {order_by: {acqpro: ['code']}}, {atomic: true})
+ .toPromise().then(providers => {
+ this.providersList = providers;
+ return providers;
+ });
+ }
+
+ getSelectionLists(): Promise<IdlObject[]> {
+ if (this.selectionLists) {
+ return Promise.resolve(this.selectionLists);
+ }
+
+ const owners = this.auth.user().id();
+ return this.pcrud.search('acqpl',
+ {owner: owners}, {order_by: {acqpl: ['name']}}, {atomic: true})
+ .toPromise().then(lists => {
+ this.selectionLists = lists;
+ return lists;
+ });
+ }
+ // Returns a promise resolved with the list of queues.
+ getAllQueues(qtype: string): Promise<IdlObject[]> {
+ if (this.allQueues[qtype]) {
+ return Promise.resolve(this.allQueues[qtype]);
+ } else {
+ this.allQueues[qtype] = [];
+ }
+
+ // could be a big list, invoke in streaming mode
+ return this.net.request(
+ 'open-ils.vandelay',
+ `open-ils.vandelay.${qtype}_queue.owner.retrieve`,
+ this.auth.token()
+ ).pipe(tap(
+ queue => this.allQueues[qtype].push(queue)
+ )).toPromise().then(() => this.allQueues[qtype]);
+ }
+
+ getBibSources(): Promise<IdlObject[]> {
+ if (this.bibSources) {
+ return Promise.resolve(this.bibSources);
+ }
+
+ return this.pcrud.retrieveAll('cbs',
+ {order_by: {cbs: 'id'}},
+ {atomic: true}
+ ).toPromise().then(sources => {
+ this.bibSources = sources;
+ return sources;
+ });
+ }
+
+ getFiscalYears(): Promise<IdlObject[]> {
+ if (this.fiscalYears) {
+ return Promise.resolve(this.fiscalYears);
+ }
+
+ return this.pcrud.retrieveAll('acqfy',
+ {order_by: {acqfy: 'year'}},
+ {atomic: true}
+ ).toPromise().then(years => {
+ this.fiscalYears = years;
+ return years;
+ });
+ }
+
+ getItemImportDefs(): Promise<IdlObject[]> {
+ if (this.importItemAttrDefs) {
+ return Promise.resolve(this.importItemAttrDefs);
+ }
+
+ const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+ return this.pcrud.search('viiad', {owner: owners}, {}, {atomic: true})
+ .toPromise().then(defs => {
+ this.importItemAttrDefs = defs;
+ return defs;
+ });
+ }
+
+ // todo: differentiate between biblio and authority a la queue api
+ getMatchSets(mtype: string): Promise<IdlObject[]> {
+
+ const mstype = mtype.match(/bib/) ? 'biblio' : 'authority';
+
+ if (this.matchSets[mtype]) {
+ return Promise.resolve(this.matchSets[mtype]);
+ } else {
+ this.matchSets[mtype] = [];
+ }
+
+ const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+
+ return this.pcrud.search('vms',
+ {owner: owners, mtype: mstype}, {}, {atomic: true})
+ .toPromise().then(sets => {
+ this.matchSets[mtype] = sets;
+ return sets;
+ });
+ }
+
+
+
+
+
+
+ // Create a queue and return the ID of the new queue via promise.
+ createQueue(
+ queueName: string,
+ recordType: string,
+ importDefId: number,
+ matchSet: number): Promise<number> {
+
+ const method = `open-ils.vandelay.${recordType}_queue.create`;
+
+ let qType = recordType;
+ if (recordType.match(/acq/)) {
+ qType = 'bib';
+ }
+
+ return new Promise((resolve, reject) => {
+ this.net.request(
+ 'open-ils.vandelay', method,
+ this.auth.token(), queueName, null, qType,
+ matchSet, importDefId
+ ).subscribe(queue => {
+ const e = this.evt.parse(queue);
+ if (e) {
+ reject(e);
+ } else {
+ // createQueue is always called after queues have
+ // been fetched and cached.
+ this.allQueues[qType].push(queue);
+ resolve(queue.id());
+ }
+ });
+ });
+ }
+
+ getQueuedRecords(queueId: number, queueType: string,
+ options?: any, limitToMatches?: boolean): Observable<any> {
+
+ const qtype = queueType.match(/bib/) ? 'bib' : 'auth';
+
+ let method =
+ `open-ils.vandelay.${qtype}_queue.records.retrieve`;
+
+ if (limitToMatches) {
+ method =
+ `open-ils.vandelay.${qtype}_queue.records.matches.retrieve`;
+ }
+
+ return this.net.request('open-ils.vandelay',
+ method, this.auth.token(), queueId, options);
+ }
+
+ // Download a queue as a MARC file.
+ exportQueue(queue: IdlObject, nonImported?: boolean) {
+
+ const etype = queue.queue_type().match(/auth/) ? 'auth' : 'bib';
+
+ let url =
+ `${VANDELAY_EXPORT_PATH}?type=bib&queueid=${queue.id()}`;
+
+ let saveName = queue.name();
+
+ if (nonImported) {
+ url += '&nonimported=1';
+ saveName += '_nonimported';
+ }
+
+ saveName += '.mrc';
+
+ this.http.get(url, {responseType: 'text'}).subscribe(
+ data => {
+ saveAs(
+ new Blob([data], {type: 'application/octet-stream'}),
+ saveName
+ );
+ },
+ err => {
+ console.error(err);
+ }
+ );
+ }
+}
+
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item"
- href="/eg/staff/acq/legacy/picklist/upload">
- <span class="material-icons" aria-hidden="true">cloud_upload</span>
- <span i18n>Load MARC Order Records</span>
- </a>
+ routerLink="/staff/acq/picklist/upload">
+ <span class="material-icons" aria-hidden="true">cloud_upload</span>
+ <span i18n>Load MARC Order Records</span>
+ </a>
<a class="dropdown-item"
routerLink="/staff/acq/search/purchaseorders">
<span class="material-icons" aria-hidden="true">shopping_cart</span>
$TEMPLATE$
);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+ 'eg.acq.picklist.upload.templates', 'acq', 'object',
+ oils_i18n_gettext(
+ 'eg.acq.picklist.upload.templates',
+ 'Picklist Upload Form Templates',
+ 'cwst', 'label'
+ )
+);
--- /dev/null
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+ 'eg.acq.picklist.upload.templates','acq','object',
+ oils_i18n_gettext(
+ 'eg.acq.picklist.upload.templates',
+ 'Acq Picklist Uploader Templates',
+ 'cwst','label'
+ )
+);
+
+COMMIT;
\ No newline at end of file
</li>
<li class="divider"></li>
<li>
- <a href="./acq/legacy/picklist/upload" target="_self">
+ <a href="/eg2/staff/acq/picklist/upload" target="_self">
<span class="glyphicon glyphicon-cloud-upload" aria-hidden="true"></span>
[% l('Load MARC Order Records') %]
</a>