241379f382d572b3fe7a70b0a0baa2c81b452739
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / share / marc-edit / editor.component.ts
1 import {Component, Input, Output, OnInit, EventEmitter, ViewChild} from '@angular/core';
2 import {IdlService} from '@eg/core/idl.service';
3 import {EventService} from '@eg/core/event.service';
4 import {NetService} from '@eg/core/net.service';
5 import {AuthService} from '@eg/core/auth.service';
6 import {OrgService} from '@eg/core/org.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {ToastService} from '@eg/share/toast/toast.service';
9 import {ServerStoreService} from '@eg/core/server-store.service';
10 import {StringComponent} from '@eg/share/string/string.component';
11 import {MarcRecord} from './marcrecord';
12 import {ComboboxEntry, ComboboxComponent
13   } from '@eg/share/combobox/combobox.component';
14 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
15 import {MarcEditContext} from './editor-context';
16 import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
17 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
18
19 export interface MarcSavedEvent {
20     marcXml: string;
21     bibSource?: number;
22     recordId?: number;
23 }
24
25 /**
26  * MARC Record editor main interface.
27  */
28
29 @Component({
30   selector: 'eg-marc-editor',
31   templateUrl: './editor.component.html'
32 })
33
34 export class MarcEditorComponent implements OnInit {
35
36     editorTab: 'rich' | 'flat';
37     sources: ComboboxEntry[];
38     context: MarcEditContext;
39
40     // True if the save request is in flight
41     dataSaving: boolean;
42
43     @Input() recordType: 'biblio' | 'authority' = 'biblio';
44
45     _pendingRecordId: number;
46     @Input() set recordId(id: number) {
47         if (this.record && this.record.id === id) { return; }
48
49         // Avoid fetching the record by ID before OnInit since we may
50         // not yet know our recordType.
51         if (this.initCalled) {
52             this._pendingRecordId = null;
53             this.fromId(id);
54
55          } else {
56             // fetch later in OnInit
57             this._pendingRecordId = id;
58          }
59     }
60
61     get recordId(): number {
62         return this.record ? this.record.id : this._pendingRecordId;
63     }
64
65     @Input() set recordXml(xml: string) {
66         if (xml) {
67             this.fromXml(xml);
68         }
69     }
70
71     get record(): MarcRecord {
72         return this.context.record;
73     }
74
75     // Tell us which record source to select by default.
76     // Useful for new records and in-place editing from bare XML.
77     @Input() recordSource: number;
78
79     // If true, saving records to the database is assumed to
80     // happen externally.  IOW, the record editor is just an
81     // in-place MARC modification interface.
82     @Input() inPlaceMode: boolean;
83
84     // In inPlaceMode, this is emitted in lieu of saving the record
85     // in th database.  When inPlaceMode is false, this is emitted after
86     // the record is successfully saved.
87     @Output() recordSaved: EventEmitter<MarcSavedEvent>;
88
89     @ViewChild('sourceSelector', {static: false}) sourceSelector: ComboboxComponent;
90     @ViewChild('confirmDelete', {static: false}) confirmDelete: ConfirmDialogComponent;
91     @ViewChild('confirmUndelete', {static: false}) confirmUndelete: ConfirmDialogComponent;
92     @ViewChild('cannotDelete', {static: false}) cannotDelete: ConfirmDialogComponent;
93     @ViewChild('successMsg', {static: false}) successMsg: StringComponent;
94     @ViewChild('failMsg', {static: false}) failMsg: StringComponent;
95
96     fastItemLabel: string;
97     fastItemBarcode: string;
98     showFastAdd: boolean;
99     initCalled = false;
100
101     constructor(
102         private evt: EventService,
103         private idl: IdlService,
104         private net: NetService,
105         private auth: AuthService,
106         private org: OrgService,
107         private pcrud: PcrudService,
108         private toast: ToastService,
109         private holdings: HoldingsService,
110         private store: ServerStoreService
111     ) {
112         this.sources = [];
113         this.recordSaved = new EventEmitter<MarcSavedEvent>();
114         this.context = new MarcEditContext();
115
116         this.recordSaved.subscribe(_ => this.dataSaving = false);
117     }
118
119     ngOnInit() {
120
121         this.initCalled = true;
122
123         this.context.recordType = this.recordType;
124
125         this.store.getItem('cat.marcedit.flateditor').then(
126             useFlat => this.editorTab = useFlat ? 'flat' : 'rich');
127
128         if (!this.record && this.recordId) {
129             this.fromId(this.recordId);
130         }
131
132         if (this.recordType !== 'biblio') { return; }
133
134         this.pcrud.retrieveAll('cbs').subscribe(
135             src => this.sources.push({id: +src.id(), label: src.source()}),
136             _ => {},
137             () => {
138                 this.sources = this.sources.sort((a, b) =>
139                     a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
140                 );
141
142                 if (this.recordSource) {
143                     this.sourceSelector.applyEntryId(this.recordSource);
144                 }
145             }
146         );
147     }
148
149     changesPending(): boolean {
150         return this.context.changesPending;
151     }
152
153     clearPendingChanges() {
154         this.context.changesPending = false;
155     }
156
157     // Remember the last used tab as the preferred tab.
158     tabChange(evt: NgbTabChangeEvent) {
159
160         // Avoid undo persistence across tabs since that could result
161         // in changes getting lost.
162         this.context.resetUndos();
163
164         if (evt.nextId === 'flat') {
165             this.store.setItem('cat.marcedit.flateditor', true);
166         } else {
167             this.store.removeItem('cat.marcedit.flateditor');
168         }
169     }
170
171     saveRecord(): Promise<any> {
172         const xml = this.record.toXml();
173         this.dataSaving = true;
174
175         // Save actions clears any pending changes.
176         this.context.changesPending = false;
177         this.context.resetUndos();
178
179         let sourceName: string = null;
180         let sourceId: number = null;
181
182         if (this.sourceSelector && this.sourceSelector.selected) {
183             sourceName = this.sourceSelector.selected.label;
184             sourceId = this.sourceSelector.selected.id;
185         }
186
187         const emission = {
188             marcXml: xml, bibSource: sourceId, recordId: this.recordId};
189
190         if (this.inPlaceMode) {
191             // Let the caller have the modified XML and move on.
192             this.recordSaved.emit(emission);
193             return Promise.resolve();
194         }
195
196         let promise;
197
198         if (this.record.id) { // Editing an existing record
199
200             promise = this.modifyRecord(xml, sourceName, sourceId);
201
202         } else {
203
204             promise = this.createRecord(xml, sourceName);
205         }
206
207         // NOTE we do not reinitialize our record with the MARC returned
208         // from the server after a create/update, which means our record
209         // may be out of sync, e.g. missing 901* values.  It's the
210         // callers responsibility to tear us down and rebuild us.
211         return promise.then(marcXml => {
212             if (!marcXml) { return null; }
213             this.successMsg.current().then(msg => this.toast.success(msg));
214             emission.marcXml = marcXml;
215             emission.recordId = this.recordId;
216             this.recordSaved.emit(emission);
217             this.fastAdd();
218             return marcXml;
219         });
220     }
221
222     modifyRecord(marcXml: string, sourceName: string, sourceId: number): Promise<any> {
223         const method = this.recordType === 'biblio' ?
224             'open-ils.cat.biblio.record.xml.update' :
225             'open-ils.cat.authority.record.overlay';
226
227         return this.net.request('open-ils.cat', method,
228             this.auth.token(), this.record.id, marcXml, sourceName
229
230         ).toPromise().then(response => {
231
232             const evt = this.evt.parse(response);
233             if (evt) {
234                 console.error(evt);
235                 this.failMsg.current().then(msg => this.toast.warning(msg));
236                 this.dataSaving = false;
237                 return null;
238             }
239
240             // authority.record.overlay resturns a '1' on success.
241             return typeof response === 'object' ? response.marc() : marcXml;
242         });
243     }
244
245     createRecord(marcXml: string, sourceName?: string): Promise<any> {
246
247         const method = this.recordType === 'biblio' ?
248             'open-ils.cat.biblio.record.xml.create' :
249             'open-ils.cat.authority.record.import';
250
251         return this.net.request('open-ils.cat', method,
252             this.auth.token(), marcXml, sourceName
253         ).toPromise().then(response => {
254
255             const evt = this.evt.parse(response);
256
257             if (evt) {
258                 console.error(evt);
259                 this.failMsg.current().then(msg => this.toast.warning(msg));
260                 this.dataSaving = false;
261                 return null;
262             }
263
264             this.record.id = response.id();
265             return response.marc();
266         });
267     }
268
269     fromId(id: number): Promise<any> {
270         const idlClass = this.recordType === 'authority' ? 'are' : 'bre';
271
272         return this.pcrud.retrieve(idlClass, id)
273         .toPromise().then(rec => {
274             this.context.record = new MarcRecord(rec.marc());
275             this.record.id = id;
276             this.record.deleted = rec.deleted() === 't';
277             if (idlClass === 'bre' && rec.source()) {
278                 this.sourceSelector.applyEntryId(+rec.source());
279             }
280         });
281     }
282
283     fromXml(xml: string) {
284         this.context.record = new MarcRecord(xml);
285         this.record.id = null;
286     }
287
288     deleteRecord(): Promise<any> {
289
290         return this.confirmDelete.open().toPromise()
291         .then(yes => {
292             if (!yes) { return; }
293
294             let promise;
295             if (this.recordType === 'authority') {
296                 promise = this.deleteAuthorityRecord();
297             } else {
298                 promise = this.deleteBibRecord();
299             }
300
301             return promise.then(ok => {
302                 if (!ok) { return; }
303
304                 return this.fromId(this.record.id).then(_ => {
305                     this.recordSaved.emit({
306                         marcXml: this.record.toXml(),
307                         recordId: this.recordId
308                     });
309                 });
310             });
311         });
312     }
313
314     deleteAuthorityRecord(): Promise<boolean> {
315         return this.pcrud.retrieve('are', this.record.id).toPromise()
316         .then(rec => this.pcrud.remove(rec).toPromise())
317         .then(resp => resp !== null);
318     }
319
320     deleteBibRecord(): Promise<boolean> {
321
322         return this.net.request('open-ils.cat',
323             'open-ils.cat.biblio.record_entry.delete',
324             this.auth.token(), this.record.id).toPromise()
325
326         .then(resp => {
327
328             const evt = this.evt.parse(resp);
329             if (evt) {
330                 if (evt.textcode === 'RECORD_NOT_EMPTY') {
331                     return this.cannotDelete.open().toPromise()
332                     .then(_ => false);
333                 } else {
334                     console.error(evt);
335                     alert(evt);
336                     return false;
337                 }
338             }
339
340             return true;
341         });
342     }
343
344     undeleteRecord(): Promise<any> {
345
346         return this.confirmUndelete.open().toPromise()
347         .then(yes => {
348             if (!yes) { return; }
349
350             let promise;
351             if (this.recordType === 'authority') {
352                 promise = this.undeleteAuthorityRecord();
353             } else {
354                 promise = this.undeleteBibRecord();
355             }
356
357             return promise.then(ok => {
358                 if (!ok) { return; }
359                 return this.fromId(this.record.id)
360                 .then(_ => {
361                     this.recordSaved.emit({
362                         marcXml: this.record.toXml(),
363                         recordId: this.recordId
364                     });
365                 });
366             });
367         });
368     }
369
370     undeleteAuthorityRecord(): Promise<any> {
371         return this.pcrud.retrieve('are', this.record.id).toPromise()
372         .then(rec => {
373             rec.deleted('f');
374             return this.pcrud.update(rec).toPromise();
375         }).then(resp => resp !== null);
376     }
377
378     undeleteBibRecord(): Promise<any> {
379
380         return this.net.request('open-ils.cat',
381             'open-ils.cat.biblio.record_entry.undelete',
382             this.auth.token(), this.record.id).toPromise()
383
384         .then(resp => {
385
386             const evt = this.evt.parse(resp);
387             if (evt) {
388                 console.error(evt);
389                 alert(evt);
390                 return false;
391             }
392
393             return true;
394         });
395     }
396
397     // Spawns the copy editor with the requested barcode and
398     // call number label.  Called after our record is saved.
399     fastAdd() {
400         if (this.showFastAdd && this.fastItemLabel && this.fastItemBarcode) {
401
402             const fastItem = {
403                 label: this.fastItemLabel,
404                 barcode: this.fastItemBarcode,
405                 fast_add: true
406             };
407
408             this.holdings.spawnAddHoldingsUi(this.recordId, null, [fastItem]);
409         }
410     }
411 }
412