]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/cat/volcopy/volcopy.component.ts
LP1888723 Angular Holdings Maintenance / Item Attributes Editor
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / cat / volcopy / volcopy.component.ts
1 import {Component, OnInit, AfterViewInit, ViewChild, Renderer2} from '@angular/core';
2 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
3 import {tap} from 'rxjs/operators';
4 import {IdlObject, IdlService} from '@eg/core/idl.service';
5 import {EventService} from '@eg/core/event.service';
6 import {OrgService} from '@eg/core/org.service';
7 import {NetService} from '@eg/core/net.service';
8 import {AuthService} from '@eg/core/auth.service';
9 import {PcrudService} from '@eg/core/pcrud.service';
10 import {HoldingsService, CallNumData} from '@eg/staff/share/holdings/holdings.service';
11 import {VolCopyContext} from './volcopy';
12 import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
13 import {AnonCacheService} from '@eg/share/util/anon-cache.service';
14 import {VolCopyService} from './volcopy.service';
15 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
16
17 const COPY_FLESH = {
18     flesh: 1,
19     flesh_fields: {
20         acp: [
21             'call_number', 'location', 'parts', 'tags',
22             'creator', 'editor', 'stat_cat_entries'
23         ],
24         acptcm: ['tag'],
25         acpt: ['tag_type']
26     }
27 };
28
29 interface EditSession {
30
31     // Unset if editing in multi-record mode
32     record_id: number;
33
34     // list of copy IDs
35     copies: number[];
36
37     // Adding to or creating new call numbers
38     raw: CallNumData[];
39
40     // Hide the volumes editor
41     hide_vols: boolean;
42
43     // Hide the copy attrs editor.
44     hide_copies: boolean;
45 }
46
47 @Component({
48   templateUrl: 'volcopy.component.html'
49 })
50 export class VolCopyComponent implements OnInit {
51
52     context: VolCopyContext;
53     loading = true;
54     sessionExpired = false;
55     printLabels = false;
56
57     tab = 'holdings'; // holdings | attrs | config
58     target: string;   // item | callnumber | record | session
59     targetId: string; // id value or session string
60
61     volsCanSave = true;
62     attrsCanSave = true;
63
64     constructor(
65         private router: Router,
66         private route: ActivatedRoute,
67         private renderer: Renderer2,
68         private evt: EventService,
69         private idl: IdlService,
70         private org: OrgService,
71         private net: NetService,
72         private auth: AuthService,
73         private pcrud: PcrudService,
74         private cache: AnonCacheService,
75         private holdings: HoldingsService,
76         private volcopy: VolCopyService
77     ) { }
78
79     ngOnInit() {
80         this.route.paramMap.subscribe(
81             (params: ParamMap) => this.negotiateRoute(params));
82     }
83
84     negotiateRoute(params: ParamMap) {
85         this.tab = params.get('tab') || 'holdings';
86         this.target = params.get('target');
87         this.targetId = params.get('target_id');
88
89         if (this.volcopy.currentContext) {
90             // Avoid clobbering the context on route change.
91             this.context = this.volcopy.currentContext;
92         } else {
93             this.context = new VolCopyContext();
94             this.context.org = this.org; // inject;
95         }
96
97         switch (this.target) {
98             case 'item':
99                 this.context.copyId = +this.targetId;
100                 break;
101             case 'callnumber':
102                 this.context.volId = +this.targetId;
103                 break;
104             case 'record':
105                 this.context.recordId = +this.targetId;
106                 break;
107             case 'session':
108                 this.context.session = this.targetId;
109                 break;
110         }
111
112         if (this.volcopy.currentContext) {
113             this.loading = false;
114
115         } else {
116             // Avoid refetching the data during route changes.
117             this.volcopy.currentContext = this.context;
118             this.load();
119         }
120     }
121
122     load(copyIds?: number[]): Promise<any> {
123         this.sessionExpired = false;
124         this.loading = true;
125         this.context.reset();
126
127         return this.volcopy.load()
128         .then(_ => this.fetchHoldings(copyIds))
129         .then(_ => this.volcopy.applyVolLabels(
130             this.context.volNodes().map(n => n.target)))
131         .then(_ => this.context.sortHoldings())
132         .then(_ => this.context.setRecordId())
133         .then(_ => this.printLabels =
134             this.volcopy.defaults.values.print_labels === true)
135         .then(_ => {
136             // unified display has no 'attrs' tab
137             if (this.volcopy.defaults.values.unified_display
138                 && this.tab === 'attrs') {
139                 this.tab = 'holdings';
140                 this.routeToTab();
141             }
142         })
143         .then(_ => this.loading = false);
144     }
145
146     fetchHoldings(copyIds?: number[]): Promise<any> {
147
148         if (copyIds && copyIds.length > 0) {
149             // Reloading copies that were just edited.
150             return this.fetchCopies(copyIds);
151
152         } else if (this.context.session) {
153             this.context.sessionType = 'mixed';
154             return this.fetchSession(this.context.session);
155
156         } else if (this.context.copyId) {
157             this.context.sessionType = 'copy';
158             return this.fetchCopies(this.context.copyId);
159
160         } else if (this.context.volId) {
161             this.context.sessionType = 'vol';
162             return this.fetchVols(this.context.volId);
163
164         } else if (this.context.recordId) {
165             this.context.sessionType = 'record';
166             return this.fetchRecords(this.context.recordId);
167         }
168     }
169
170     // Changing a tab in the UI means changing the route.
171     // Changing the route ultimately results in changing the tab.
172     beforeTabChange(evt: NgbNavChangeEvent) {
173         evt.preventDefault();
174         this.tab = evt.nextId;
175         this.routeToTab();
176     }
177
178     routeToTab() {
179         const url =
180             `/staff/cat/volcopy/${this.tab}/${this.target}/${this.targetId}`;
181
182         // Retain search parameters
183         this.router.navigate([url], {queryParamsHandling: 'merge'});
184     }
185
186     fetchSession(session: string): Promise<any> {
187
188         return this.cache.getItem(session, 'edit-these-copies')
189         .then((editSession: EditSession) => {
190
191             if (!editSession) {
192                 this.loading = false;
193                 this.sessionExpired = true;
194                 return Promise.reject('Session Expired');
195             }
196
197             console.debug('Edit Session', editSession);
198
199             this.context.recordId = editSession.record_id;
200
201             if (editSession.copies && editSession.copies.length > 0) {
202                 return this.fetchCopies(editSession.copies);
203             }
204
205             const volsToFetch = [];
206             const volsToCreate = [];
207             editSession.raw.forEach((volData: CallNumData) => {
208                 this.context.fastAdd = volData.fast_add === true;
209
210                 if (volData.callnumber > 0) {
211                     volsToFetch.push(volData);
212                 } else {
213                     volsToCreate.push(volData);
214                 }
215             });
216
217             let promise = Promise.resolve();
218             if (volsToFetch.length > 0) {
219                 promise = promise.then(_ =>
220                     this.fetchVolsStubCopies(volsToFetch));
221             }
222
223             if (volsToCreate.length > 0) {
224                 promise = promise.then(_ =>
225                     this.createVolsStubCopies(volsToCreate));
226             }
227
228             return promise;
229         });
230     }
231
232     // Creating new vols.  Each gets a stub copy.
233     createVolsStubCopies(volDataList: CallNumData[]): Promise<any> {
234
235         const vols = [];
236         volDataList.forEach(volData => {
237
238             const vol = this.volcopy.createStubVol(
239                 this.context.recordId,
240                 volData.owner || this.auth.user().ws_ou()
241             );
242
243             if (volData.label) {vol.label(volData.label); }
244
245             volData.callnumber = vol.id(); // wanted by addStubCopies
246             vols.push(vol);
247             this.context.findOrCreateVolNode(vol);
248         });
249
250         return this.addStubCopies(vols, volDataList)
251         .then(_ => this.volcopy.setVolClassLabels(vols));
252     }
253
254     // Fetch vols by ID, but instead of retrieving their copies
255     // add a stub copy to each.
256     fetchVolsStubCopies(volDataList: CallNumData[]): Promise<any> {
257
258         const volIds = volDataList.map(volData => volData.callnumber);
259         const vols = [];
260
261         return this.pcrud.search('acn', {id: volIds})
262         .pipe(tap((vol: IdlObject) => vols.push(vol))).toPromise()
263         .then(_ => this.addStubCopies(vols, volDataList));
264     }
265
266     // Add a stub copy to each vol using data from the edit session.
267     addStubCopies(vols: IdlObject[], volDataList: CallNumData[]): Promise<any> {
268
269         const copies = [];
270         vols.forEach(vol => {
271             const volData = volDataList.filter(
272                 vData => vData.callnumber === vol.id())[0];
273
274             const copy =
275                 this.volcopy.createStubCopy(vol, {circLib: volData.owner});
276
277             this.context.findOrCreateCopyNode(copy);
278             copies.push(copy);
279         });
280
281         return this.volcopy.setCopyStatus(copies, this.context.fastAdd);
282     }
283
284     fetchCopies(copyIds: number | number[]): Promise<any> {
285         const ids = [].concat(copyIds);
286         return this.pcrud.search('acp', {id: ids}, COPY_FLESH)
287         .pipe(tap(copy => this.context.findOrCreateCopyNode(copy)))
288         .toPromise();
289     }
290
291     // Fetch call numbers and linked copies by call number ids.
292     fetchVols(volIds?: number | number[]): Promise<any> {
293         const ids = [].concat(volIds);
294
295         return this.pcrud.search('acn', {id: ids})
296         .pipe(tap(vol => this.context.findOrCreateVolNode(vol)))
297         .toPromise().then(_ => {
298              return this.pcrud.search('acp',
299                 {call_number: ids, deleted: 'f'}, COPY_FLESH
300             ).pipe(tap(copy => this.context.findOrCreateCopyNode(copy))
301             ).toPromise();
302         });
303     }
304
305     // Fetch call numbers and copies by record ids.
306     fetchRecords(recordIds: number | number[]): Promise<any> {
307         const ids = [].concat(recordIds);
308
309         return this.pcrud.search('acn',
310             {record: ids, deleted: 'f', label: {'!=' : '##URI##'}},
311             {}, {idlist: true, atomic: true}
312         ).toPromise().then(volIds => this.fetchVols(volIds));
313     }
314
315
316     save(close?: boolean): Promise<any> {
317         this.loading = true;
318
319         // Volume update API wants volumes fleshed with copies, instead
320         // of the other way around, which is what we have here.
321         const volumes: IdlObject[] = [];
322
323         this.context.volNodes().forEach(volNode => {
324             const newVol = this.idl.clone(volNode.target);
325             const copies: IdlObject[] = [];
326
327             volNode.children.forEach(copyNode => {
328                 const copy = copyNode.target;
329
330                 if (copy.isnew() && !copy.barcode()) {
331                     // A new copy w/ no barcode is a stub copy sitting
332                     // on an empty call number.  Ignore it.
333                     return;
334                 }
335
336                 if (copy.ischanged() || copy.isnew() || copy.isdeleted()) {
337                     const copyClone = this.idl.clone(copy);
338                     // De-flesh call number
339                     copyClone.call_number(copy.call_number().id());
340                     copies.push(copyClone);
341                 }
342             });
343
344            newVol.copies(copies);
345
346             if (newVol.ischanged() || newVol.isnew() || copies.length > 0) {
347                 volumes.push(newVol);
348             }
349         });
350
351         this.context.volsToDelete.forEach(vol => {
352             const cloneVol = this.idl.clone(vol);
353             // No need to flesh copies -- they'll be force deleted.
354             cloneVol.copies([]);
355             volumes.push(cloneVol);
356         });
357
358         this.context.copiesToDelete.forEach(copy => {
359             const cloneCopy = this.idl.clone(copy);
360             const copyVol = cloneCopy.call_number();
361             cloneCopy.call_number(copyVol.id()); // de-flesh
362
363             let vol = volumes.filter(v => v.id() === copyVol.id())[0];
364
365             if (vol) {
366                 vol.copies().push(cloneCopy);
367             } else {
368                 vol = this.idl.clone(copyVol);
369                 vol.copies([cloneCopy]);
370             }
371
372             volumes.push(vol);
373         });
374
375         // De-flesh before posting
376         volumes.forEach(vol => {
377             vol.copies().forEach(copy => {
378                 ['editor', 'creator', 'location'].forEach(field => {
379                     if (typeof copy[field]() === 'object') {
380                         copy[field](copy[field]().id());
381                     }
382                 });
383             });
384         });
385
386         let promise: Promise<number[]> = Promise.resolve([]);
387
388         if (volumes.length > 0) {
389             promise = this.saveApi(volumes, false, close);
390         }
391
392         return promise.then(copyIds => {
393
394             // In addition to the copies edited in this update call,
395             // reload any other copies that were previously loaded.
396             const ids: any = {}; // dedupe
397             this.context.copyList()
398                 .map(c => c.id())
399                 .filter(id => id > 0) // scrub the new copy IDs
400                 .concat(copyIds)
401                 .forEach(id => ids[id] = true);
402
403             copyIds = Object.keys(ids).map(id => Number(id));
404
405             if (close) {
406                 return this.openPrintLabels(copyIds)
407                     .then(_ => setTimeout(() => window.close()));
408             }
409
410             return this.load(Object.keys(ids).map(id => Number(id)));
411
412         }).then(_ => this.loading = false);
413     }
414
415     saveApi(volumes: IdlObject[], override?:
416         boolean, close?: boolean): Promise<number[]> {
417
418         let method = 'open-ils.cat.asset.volume.fleshed.batch.update';
419         if (override) { method += '.override'; }
420
421         return this.net.request('open-ils.cat',
422             method, this.auth.token(), volumes, true,
423             {   auto_merge_vols: true,
424                 create_parts: true,
425                 return_copy_ids: true,
426                 force_delete_copies: true
427             }
428
429         ).toPromise().then(copyIds => {
430
431             const evt = this.evt.parse(copyIds);
432
433             if (evt) {
434                 // TODO: handle overrides?
435                 // return this.saveApi(volumes, true, close);
436                 this.loading = false;
437                 alert(evt);
438                 return Promise.reject();
439             }
440
441             return copyIds;
442         });
443     }
444
445     savePrintLabels() {
446         this.volcopy.defaults.values.print_labels = this.printLabels === true;
447         this.volcopy.saveDefaults();
448     }
449
450     openPrintLabels(copyIds?: number[]): Promise<any> {
451         if (!this.printLabels) { return Promise.resolve(); }
452
453         if (!copyIds || copyIds.length === 0) {
454             copyIds = this.context.copyList()
455                 .map(c => c.id()).filter(id => id > 0);
456         }
457
458         return this.net.request(
459             'open-ils.actor',
460             'open-ils.actor.anon_cache.set_value',
461             null, 'print-labels-these-copies', {copies : copyIds}
462
463         ).toPromise().then(key => {
464
465             const url = '/eg/staff/cat/printlabels/' + key;
466             setTimeout(() => window.open(url, '_blank'));
467         });
468     }
469
470     isNotSaveable(): boolean {
471         return !(this.volsCanSave && this.attrsCanSave);
472     }
473 }
474
475
476