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