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