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