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