]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.ts
LP1919465 Pull list only shows holds with current_copy
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / share / holds / grid.component.ts
1 import {Component, OnInit, Input, ViewChild} from '@angular/core';
2 import {Observable, Observer, of} from 'rxjs';
3 import {IdlObject} from '@eg/core/idl.service';
4 import {NetService} from '@eg/core/net.service';
5 import {OrgService} from '@eg/core/org.service';
6 import {AuthService} from '@eg/core/auth.service';
7 import {Pager} from '@eg/share/util/pager';
8 import {ServerStoreService} from '@eg/core/server-store.service';
9 import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
10 import {GridComponent} from '@eg/share/grid/grid.component';
11 import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
12 import {MarkDamagedDialogComponent
13     } from '@eg/staff/share/holdings/mark-damaged-dialog.component';
14 import {MarkMissingDialogComponent
15     } from '@eg/staff/share/holdings/mark-missing-dialog.component';
16 import {MarkDiscardDialogComponent
17     } from '@eg/staff/share/holdings/mark-discard-dialog.component';
18 import {HoldRetargetDialogComponent
19     } from '@eg/staff/share/holds/retarget-dialog.component';
20 import {HoldTransferDialogComponent} from './transfer-dialog.component';
21 import {HoldCancelDialogComponent} from './cancel-dialog.component';
22 import {HoldManageDialogComponent} from './manage-dialog.component';
23 import {PrintService} from '@eg/share/print/print.service';
24 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
25
26 /** Holds grid with access to detail page and other actions */
27
28 @Component({
29   selector: 'eg-holds-grid',
30   templateUrl: 'grid.component.html'
31 })
32 export class HoldsGridComponent implements OnInit {
33
34     // If either are set/true, the pickup lib selector will display
35     @Input() initialPickupLib: number | IdlObject;
36     @Input() hidePickupLibFilter: boolean;
37
38     // Setting a value here puts us into "pull list" mode.
39     @Input() pullListOrg: number;
40
41     // If true, only retrieve holds with a Hopeless Date
42     // and enable related Actions
43     @Input() hopeless: boolean;
44
45     // Grid persist key
46     @Input() persistKey: string;
47
48     @Input() preFetchSetting: string;
49
50     @Input() printTemplate: string;
51
52     // If set, all holds are fetched on grid load and sorting/paging all
53     // happens in the client.  If false, sorting and paging occur on
54     // the server.
55     @Input() enablePreFetch: boolean;
56
57     // How to sort when no sort parameters have been applied
58     // via grid controls.  This uses the eg-grid sort format:
59     // [{name: fname, dir: 'asc'}, {name: fname2, dir: 'desc'}]
60     @Input() defaultSort: any[];
61
62     // To pass through to the underlying eg-grid
63     @Input() showFields: string;
64
65     // Display bib record summary along the top of the detail page.
66     @Input() showRecordSummary = false;
67
68     mode: 'list' | 'detail' | 'manage' = 'list';
69     initDone = false;
70     holdsCount: number;
71     pickupLib: IdlObject;
72     plCompLoaded = false;
73     gridDataSource: GridDataSource;
74     detailHold: any;
75     editHolds: number[];
76     transferTarget: number;
77
78     @ViewChild('holdsGrid', { static: false }) private holdsGrid: GridComponent;
79     @ViewChild('progressDialog', { static: true })
80         private progressDialog: ProgressDialogComponent;
81     @ViewChild('transferDialog', { static: true })
82         private transferDialog: HoldTransferDialogComponent;
83     @ViewChild('markDamagedDialog', { static: true })
84         private markDamagedDialog: MarkDamagedDialogComponent;
85     @ViewChild('markMissingDialog', { static: true })
86         private markMissingDialog: MarkMissingDialogComponent;
87     @ViewChild('markDiscardDialog')
88         private markDiscardDialog: MarkDiscardDialogComponent;
89     @ViewChild('retargetDialog', { static: true })
90         private retargetDialog: HoldRetargetDialogComponent;
91     @ViewChild('cancelDialog', { static: true })
92         private cancelDialog: HoldCancelDialogComponent;
93     @ViewChild('manageDialog', { static: true })
94         private manageDialog: HoldManageDialogComponent;
95
96     // Bib record ID.
97     _recordId: number;
98     @Input() set recordId(id: number) {
99         this._recordId = id;
100         if (this.initDone) { // reload on update
101             this.holdsGrid.reload();
102         }
103     }
104
105     _userId: number;
106     @Input() set userId(id: number) {
107         this._userId = id;
108         if (this.initDone) {
109             this.holdsGrid.reload();
110         }
111     }
112
113     // Include holds canceled on or after the provided date.
114     // If no value is passed, canceled holds are not displayed.
115     _showCanceledSince: Date;
116     @Input() set showCanceledSince(show: Date) {
117         this._showCanceledSince = show;
118         if (this.initDone) { // reload on update
119             this.holdsGrid.reload();
120         }
121     }
122
123     // Include holds fulfilled on or after hte provided date.
124     // If no value is passed, fulfilled holds are not displayed.
125     _showFulfilledSince: Date;
126     @Input() set showFulfilledSince(show: Date) {
127         this._showFulfilledSince = show;
128         if (this.initDone) { // reload on update
129             this.holdsGrid.reload();
130         }
131     }
132
133
134     cellTextGenerator: GridCellTextGenerator;
135
136     // Include holds marked Hopeless on or after this date.
137     _showHopelessAfter: Date;
138     @Input() set showHopelessAfter(show: Date) {
139         this._showHopelessAfter = show;
140         if (this.initDone) { // reload on update
141             this.holdsGrid.reload();
142         }
143     }
144
145     // Include holds marked Hopeless on or before this date.
146     _showHopelessBefore: Date;
147     @Input() set showHopelessBefore(show: Date) {
148         this._showHopelessBefore = show;
149         if (this.initDone) { // reload on update
150             this.holdsGrid.reload();
151         }
152     }
153
154     constructor(
155         private net: NetService,
156         private org: OrgService,
157         private store: ServerStoreService,
158         private auth: AuthService,
159         private printer: PrintService,
160         private holdings: HoldingsService
161     ) {
162         this.gridDataSource = new GridDataSource();
163         this.enablePreFetch = null;
164     }
165
166     ngOnInit() {
167         this.initDone = true;
168         this.pickupLib = this.org.get(this.initialPickupLib);
169
170         if (this.preFetchSetting) {
171             this.store.getItem(this.preFetchSetting).then(
172                 applied => this.enablePreFetch = Boolean(applied)
173             );
174         }
175
176         if (!this.defaultSort) {
177             if (this.pullListOrg) {
178
179                 this.defaultSort = [
180                     {name: 'copy_location_order_position', dir: 'asc'},
181                     {name: 'acpl_name', dir: 'asc'},
182                     {name: 'ancp_label', dir: 'asc'}, // NOTE: API typo "ancp"
183                     {name: 'cn_label_sortkey', dir: 'asc'},
184                     {name: 'ancs_label', dir: 'asc'} // NOTE: API typo "ancs"
185                 ];
186
187             } else {
188                 this.defaultSort = [{name: 'request_time', dir: 'asc'}];
189             }
190         }
191
192         this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
193
194             if (!this.hidePickupLibFilter && !this.plCompLoaded) {
195                 // When the pickup lib selector is active, avoid any
196                 // data fetches until it has settled on a default value.
197                 // Once the final value is applied, its onchange will
198                 // fire and we'll be back here with plCompLoaded=true.
199                 return of([]);
200             }
201
202             sort = sort.length > 0 ? sort : this.defaultSort;
203             return this.fetchHolds(pager, sort);
204         };
205
206         // Text-ify function for cells that use display templates.
207         this.cellTextGenerator = {
208             title: row => row.title,
209             cp_barcode: row => (row.cp_barcode == null) ? '' : row.cp_barcode,
210             ucard_barcode: row => row.ucard_barcode
211         };
212     }
213
214     // Returns true after all data/settings/etc required to render the
215     // grid have been fetched.
216     initComplete(): boolean {
217         return this.enablePreFetch !== null;
218     }
219
220     pickupLibChanged(org: IdlObject) {
221         this.pickupLib = org;
222         this.holdsGrid.reload();
223     }
224
225     pullListOrgChanged(org: IdlObject) {
226         this.pullListOrg = org.id();
227         this.holdsGrid.reload();
228     }
229
230     preFetchHolds(apply: boolean) {
231         this.enablePreFetch = apply;
232
233         if (apply) {
234             setTimeout(() => this.holdsGrid.reload());
235         }
236
237         if (this.preFetchSetting) {
238             // fire and forget
239             this.store.setItem(this.preFetchSetting, apply);
240         }
241     }
242
243     applyFilters(): any {
244
245         const filters: any = {};
246
247         if (this.pullListOrg) {
248             filters.cancel_time = null;
249             filters.capture_time = null;
250             filters.frozen = 'f';
251
252             // cp.* fields are set for copy-level holds even if they
253             // have no current_copy.  Make sure current_copy is set.
254             filters.current_copy = {'is not': null};
255
256             // There are aliases for these (cp_status, cp_circ_lib),
257             // but the API complains when I use them.
258             filters['cp.status'] = [0, 7];
259             filters['cp.circ_lib'] = this.pullListOrg;
260
261             return filters;
262         }
263
264         if (this._showFulfilledSince) {
265             filters.fulfillment_time = this._showFulfilledSince.toISOString();
266         } else {
267             filters.fulfillment_time = null;
268         }
269
270         if (this._showCanceledSince) {
271             filters.cancel_time = this._showCanceledSince.toISOString();
272         } else {
273             filters.cancel_time = null;
274         }
275
276         if (this.hopeless) {
277           filters['hopeless_holds'] = {
278             'start_date' : this._showHopelessAfter
279               ? (
280                   // FIXME -- consistency desired, string or object
281                   typeof this._showHopelessAfter === 'object'
282                   ? this._showHopelessAfter.toISOString()
283                   : this._showHopelessAfter
284                 )
285               : '1970-01-01T00:00:00.000Z',
286             'end_date' : this._showHopelessBefore
287               ? (
288                   // FIXME -- consistency desired, string or object
289                   typeof this._showHopelessBefore === 'object'
290                   ? this._showHopelessBefore.toISOString()
291                   : this._showHopelessBefore
292                 )
293               : (new Date()).toISOString()
294           };
295         }
296
297         if (this.pickupLib) {
298             filters.pickup_lib =
299                 this.org.descendants(this.pickupLib, true);
300         }
301
302         if (this._recordId) {
303             filters.record_id = this._recordId;
304         }
305
306         if (this._userId) {
307             filters.usr_id = this._userId;
308         }
309
310         return filters;
311     }
312
313     fetchHolds(pager: Pager, sort: any[]): Observable<any> {
314
315         // We need at least one filter.
316         if (!this._recordId && !this.pickupLib && !this._userId && !this.pullListOrg) {
317             return of([]);
318         }
319
320         const filters = this.applyFilters();
321
322         const orderBy: any = [];
323         if (sort.length > 0) {
324             sort.forEach(obj => {
325                 const subObj: any = {};
326                 subObj[obj.name] = {dir: obj.dir, nulls: 'last'};
327                 orderBy.push(subObj);
328             });
329         }
330
331         const limit = this.enablePreFetch ? null : pager.limit;
332         const offset = this.enablePreFetch ? 0 : pager.offset;
333
334         let observer: Observer<any>;
335         const observable = new Observable(obs => observer = obs);
336
337         this.progressDialog.open();
338         this.progressDialog.update({value: 0, max: 1});
339         let first = true;
340         let loadCount = 0;
341         this.net.request(
342             'open-ils.circ',
343             'open-ils.circ.hold.wide_hash.stream',
344             this.auth.token(), filters, orderBy, limit, offset
345         ).subscribe(
346             holdData => {
347
348                 if (first) { // First response is the hold count.
349                     this.holdsCount = Number(holdData);
350                     first = false;
351
352                 } else { // Subsequent responses are hold data blobs
353
354                     this.progressDialog.update(
355                         {value: ++loadCount, max: this.holdsCount});
356
357                     observer.next(holdData);
358                 }
359             },
360             err => {
361                 this.progressDialog.close();
362                 observer.error(err);
363             },
364             ()  => {
365                 this.progressDialog.close();
366                 observer.complete();
367             }
368         );
369
370         return observable;
371     }
372
373     metaRecordHoldsSelected(rows: IdlObject[]) {
374         let found = false;
375         rows.forEach( row => {
376            if (row.hold_type === 'M') {
377              found = true;
378            }
379         });
380         return found;
381     }
382
383     nonTitleHoldsSelected(rows: IdlObject[]) {
384         let found = false;
385         rows.forEach( row => {
386            if (row.hold_type !== 'T') {
387              found = true;
388            }
389         });
390         return found;
391     }
392
393     showDetails(rows: any[]) {
394         this.showDetail(rows[0]);
395     }
396
397     showDetail(row: any) {
398         if (row) {
399             this.mode = 'detail';
400             this.detailHold = row;
401         }
402     }
403
404     showManager(rows: any[]) {
405         if (rows.length) {
406             this.mode = 'manage';
407             this.editHolds = rows.map(r => r.id);
408         }
409     }
410
411     handleModify(rowsModified: boolean) {
412         this.mode = 'list';
413
414         if (rowsModified) {
415             // give the grid a chance to render then ask it to reload
416             setTimeout(() => this.holdsGrid.reload());
417         }
418     }
419
420
421
422     showRecentCircs(rows: any[]) {
423         const copyIds = Array.from(new Set( rows.map(r => r.cp_id).filter( cp_id => Boolean(cp_id)) ));
424         copyIds.forEach( copyId => {
425             const url =
426                 '/eg/staff/cat/item/' + copyId + '/circ_list';
427             window.open(url, '_blank');
428         });
429     }
430
431     showPatron(rows: any[]) {
432         const usrIds = Array.from(new Set( rows.map(r => r.usr_id).filter( usr_id => Boolean(usr_id)) ));
433         usrIds.forEach( usrId => {
434             const url =
435                 '/eg/staff/circ/patron/' + usrId + '/checkout';
436             window.open(url, '_blank');
437         });
438     }
439
440     showOrder(rows: any[]) {
441         // Doesn't work in Typescript currently without compiler option:
442         //   const bibIds = [...new Set( rows.map(r => r.record_id) )];
443         const bibIds = Array.from(
444           new Set( rows.filter(r => r.hold_type !== 'M').map(r => r.record_id) ));
445         bibIds.forEach( bibId => {
446           const url =
447               '/eg/staff/acq/legacy/lineitem/related/' + bibId + '?target=bib';
448           window.open(url, '_blank');
449         });
450     }
451
452     addVolume(rows: any[]) {
453         const bibIds = Array.from(
454           new Set( rows.filter(r => r.hold_type !== 'M').map(r => r.record_id) ));
455         bibIds.forEach( bibId => {
456           this.holdings.spawnAddHoldingsUi(bibId);
457         });
458     }
459
460     showTitle(rows: any[]) {
461         const bibIds = Array.from(new Set( rows.map(r => r.record_id) ));
462         bibIds.forEach( bibId => {
463           // const url = '/eg/staff/cat/catalog/record/' + bibId;
464           const url = '/eg2/staff/catalog/record/' + bibId;
465           window.open(url, '_blank');
466         });
467     }
468
469     showManageDialog(rows: any[]) {
470         const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
471         if (holdIds.length > 0) {
472             this.manageDialog.holdIds = holdIds;
473             this.manageDialog.open({size: 'lg'}).subscribe(
474                 rowsModified => {
475                     if (rowsModified) {
476                         this.holdsGrid.reload();
477                     }
478                 }
479             );
480         }
481     }
482
483     showTransferDialog(rows: any[]) {
484         const holdIds = rows.filter(r => r.hold_type === 'T').map(r => r.id).filter(id => Boolean(id));
485         if (holdIds.length > 0) {
486             this.transferDialog.holdIds = holdIds;
487             this.transferDialog.open({}).subscribe(
488                 rowsModified => {
489                     if (rowsModified) {
490                         this.holdsGrid.reload();
491                     }
492                 }
493             );
494         }
495     }
496
497     async showMarkDamagedDialog(rows: any[]) {
498         const copyIds = rows.map(r => r.cp_id).filter(id => Boolean(id));
499         if (copyIds.length === 0) { return; }
500
501         let rowsModified = false;
502
503         const markNext = async(ids: number[]) => {
504             if (ids.length === 0) {
505                 return Promise.resolve();
506             }
507
508             this.markDamagedDialog.copyId = ids.pop();
509             return this.markDamagedDialog.open({size: 'lg'}).subscribe(
510                 ok => {
511                     if (ok) { rowsModified = true; }
512                     return markNext(ids);
513                 },
514                 dismiss => markNext(ids)
515             );
516         };
517
518         await markNext(copyIds);
519         if (rowsModified) {
520             this.holdsGrid.reload();
521         }
522     }
523
524     showMarkMissingDialog(rows: any[]) {
525         const copyIds = rows.map(r => r.cp_id).filter(id => Boolean(id));
526         if (copyIds.length > 0) {
527             this.markMissingDialog.copyIds = copyIds;
528             this.markMissingDialog.open({}).subscribe(
529                 rowsModified => {
530                     if (rowsModified) {
531                         this.holdsGrid.reload();
532                     }
533                 }
534             );
535         }
536     }
537
538     showMarkDiscardDialog(rows: any[]) {
539         const copyIds = rows.map(r => r.cp_id).filter(id => Boolean(id));
540         if (copyIds.length > 0) {
541             this.markDiscardDialog.copyIds = copyIds;
542             this.markDiscardDialog.open({}).subscribe(
543                 rowsModified => {
544                     if (rowsModified) {
545                         this.holdsGrid.reload();
546                     }
547                 }
548             );
549         }
550     }
551
552
553     showRetargetDialog(rows: any[]) {
554         const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
555         if (holdIds.length > 0) {
556             this.retargetDialog.holdIds = holdIds;
557             this.retargetDialog.open({}).subscribe(
558                 rowsModified => {
559                     if (rowsModified) {
560                         this.holdsGrid.reload();
561                     }
562                 }
563             );
564         }
565     }
566
567     showCancelDialog(rows: any[]) {
568         const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
569         if (holdIds.length > 0) {
570             this.cancelDialog.holdIds = holdIds;
571             this.cancelDialog.open({}).subscribe(
572                 rowsModified => {
573                     if (rowsModified) {
574                         this.holdsGrid.reload();
575                     }
576                 }
577             );
578         }
579     }
580
581     printHolds() {
582         // Request a page with no limit to get all of the wide holds for
583         // printing.  Call requestPage() directly instead of grid.reload()
584         // since we may already have the data.
585
586         const pager = new Pager();
587         pager.offset = 0;
588         pager.limit = null;
589
590         if (this.gridDataSource.sort.length === 0) {
591             this.gridDataSource.sort = this.defaultSort;
592         }
593
594         this.gridDataSource.requestPage(pager).then(() => {
595             if (this.gridDataSource.data.length > 0) {
596                 this.printer.print({
597                     templateName: this.printTemplate || 'holds_for_bib',
598                     contextData: this.gridDataSource.data,
599                     printContext: 'default'
600                 });
601             }
602         });
603     }
604
605     isCopyHold(holdData: any): boolean {
606         return holdData.hold_type.match(/C|R|F/) !== null;
607     }
608 }
609
610
611
612