LP1818288 Ang staff catalog record detail holds tab/actions
[working/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 {GridDataSource} from '@eg/share/grid/grid';
9 import {GridComponent} from '@eg/share/grid/grid.component';
10 import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
11 import {MarkDamagedDialogComponent
12     } from '@eg/staff/share/holdings/mark-damaged-dialog.component';
13 import {MarkMissingDialogComponent
14     } from '@eg/staff/share/holdings/mark-missing-dialog.component';
15 import {HoldRetargetDialogComponent
16     } from '@eg/staff/share/holds/retarget-dialog.component';
17 import {HoldTransferDialogComponent} from './transfer-dialog.component';
18 import {HoldCancelDialogComponent} from './cancel-dialog.component';
19 import {HoldManageDialogComponent} from './manage-dialog.component';
20
21 /** Holds grid with access to detail page and other actions */
22
23 @Component({
24   selector: 'eg-holds-grid',
25   templateUrl: 'grid.component.html'
26 })
27 export class HoldsGridComponent implements OnInit {
28
29     // If either are set/true, the pickup lib selector will display
30     @Input() initialPickupLib: number | IdlObject;
31     @Input() hidePickupLibFilter: boolean;
32
33     // Grid persist key
34     @Input() persistKey: string;
35
36     // How to sort when no sort parameters have been applied
37     // via grid controls.  This uses the eg-grid sort format:
38     // [{name: fname, dir: 'asc'}, {name: fname2, dir: 'desc'}]
39     @Input() defaultSort: any[];
40
41     mode: 'list' | 'detail' | 'manage' = 'list';
42     initDone = false;
43     holdsCount: number;
44     pickupLib: IdlObject;
45     gridDataSource: GridDataSource;
46     detailHold: any;
47     editHolds: number[];
48     transferTarget: number;
49     copyStatuses: {[id: string]: IdlObject};
50
51     @ViewChild('holdsGrid') private holdsGrid: GridComponent;
52     @ViewChild('progressDialog')
53         private progressDialog: ProgressDialogComponent;
54     @ViewChild('transferDialog')
55         private transferDialog: HoldTransferDialogComponent;
56     @ViewChild('markDamagedDialog')
57         private markDamagedDialog: MarkDamagedDialogComponent;
58     @ViewChild('markMissingDialog')
59         private markMissingDialog: MarkMissingDialogComponent;
60     @ViewChild('retargetDialog')
61         private retargetDialog: HoldRetargetDialogComponent;
62     @ViewChild('cancelDialog')
63         private cancelDialog: HoldCancelDialogComponent;
64     @ViewChild('manageDialog')
65         private manageDialog: HoldManageDialogComponent;
66
67     // Bib record ID.
68     _recordId: number;
69     @Input() set recordId(id: number) {
70         this._recordId = id;
71         if (this.initDone) { // reload on update
72             this.holdsGrid.reload();
73         }
74     }
75
76     _userId: number;
77     @Input() set userId(id: number) {
78         this._userId = id;
79         if (this.initDone) {
80             this.holdsGrid.reload();
81         }
82     }
83
84     // Include holds canceled on or after the provided date.
85     // If no value is passed, canceled holds are not displayed.
86     _showCanceledSince: Date;
87     @Input() set showCanceledSince(show: Date) {
88         this._showCanceledSince = show;
89         if (this.initDone) { // reload on update
90             this.holdsGrid.reload();
91         }
92     }
93
94     // Include holds fulfilled on or after hte provided date.
95     // If no value is passed, fulfilled holds are not displayed.
96     _showFulfilledSince: Date;
97     @Input() set showFulfilledSince(show: Date) {
98         this._showFulfilledSince = show;
99         if (this.initDone) { // reload on update
100             this.holdsGrid.reload();
101         }
102     }
103
104     constructor(
105         private net: NetService,
106         private org: OrgService,
107         private auth: AuthService
108     ) {
109         this.gridDataSource = new GridDataSource();
110         this.copyStatuses = {};
111     }
112
113     ngOnInit() {
114         this.initDone = true;
115         this.pickupLib = this.org.get(this.initialPickupLib);
116
117         this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
118
119             if (this.defaultSort && sort.length === 0) {
120                 // Only use initial sort if sorting has not been modified
121                 // by the grid's own sort controls.
122                 sort = this.defaultSort;
123             }
124
125             // sorting not currently supported
126             return this.fetchHolds(pager, sort);
127         };
128     }
129
130     pickupLibChanged(org: IdlObject) {
131         this.pickupLib = org;
132         this.holdsGrid.reload();
133     }
134
135     applyFilters(): any {
136         const filters: any = {
137             is_staff_request: true,
138             fulfillment_time: this._showFulfilledSince ?
139                 this._showFulfilledSince.toISOString() : null,
140             cancel_time: this._showCanceledSince ?
141                 this._showCanceledSince.toISOString() : null,
142         };
143
144         if (this.pickupLib) {
145             filters.pickup_lib =
146                 this.org.descendants(this.pickupLib, true);
147         }
148
149         if (this._recordId) {
150             filters.record_id = this._recordId;
151         }
152
153         if (this._userId) {
154             filters.usr_id = this._userId;
155         }
156
157         return filters;
158     }
159
160     fetchHolds(pager: Pager, sort: any[]): Observable<any> {
161
162         // We need at least one filter.
163         if (!this._recordId && !this.pickupLib && !this._userId) {
164             return of([]);
165         }
166
167         const filters = this.applyFilters();
168
169         const orderBy: any = [];
170         sort.forEach(obj => {
171             const subObj: any = {};
172             subObj[obj.name] = {dir: obj.dir, nulls: 'last'};
173             orderBy.push(subObj);
174         });
175
176         let observer: Observer<any>;
177         const observable = new Observable(obs => observer = obs);
178
179         this.progressDialog.open();
180         this.progressDialog.update({value: 0, max: 1});
181         let first = true;
182         let loadCount = 0;
183         this.net.request(
184             'open-ils.circ',
185             'open-ils.circ.hold.wide_hash.stream',
186             // Pre-fetch all holds consistent with AngJS version
187             this.auth.token(), filters, orderBy
188             // Alternatively, fetch holds in pages.
189             // this.auth.token(), filters, orderBy, pager.limit, pager.offset
190         ).subscribe(
191             holdData => {
192
193                 if (first) { // First response is the hold count.
194                     this.holdsCount = Number(holdData);
195                     first = false;
196
197                 } else { // Subsequent responses are hold data blobs
198
199                     this.progressDialog.update(
200                         {value: ++loadCount, max: this.holdsCount});
201
202                     observer.next(holdData);
203                 }
204             },
205             err => {
206                 this.progressDialog.close();
207                 observer.error(err);
208             },
209             ()  => {
210                 this.progressDialog.close();
211                 observer.complete();
212             }
213         );
214
215         return observable;
216     }
217
218     showDetails(rows: any[]) {
219         this.showDetail(rows[0]);
220     }
221
222     showDetail(row: any) {
223         if (row) {
224             this.mode = 'detail';
225             this.detailHold = row;
226         }
227     }
228
229     showManager(rows: any[]) {
230         if (rows.length) {
231             this.mode = 'manage';
232             this.editHolds = rows.map(r => r.id);
233         }
234     }
235
236     handleModify(rowsModified: boolean) {
237         this.mode = 'list';
238
239         if (rowsModified) {
240             // give the grid a chance to render then ask it to reload
241             setTimeout(() => this.holdsGrid.reload());
242         }
243     }
244
245
246
247     showRecentCircs(rows: any[]) {
248         if (rows.length) {
249             const url =
250                 '/eg/staff/cat/item/' + rows[0].cp_id + '/circ_list';
251             window.open(url, '_blank');
252         }
253     }
254
255     showPatron(rows: any[]) {
256         if (rows.length) {
257             const url =
258                 '/eg/staff/circ/patron/' + rows[0].usr_id + '/checkout';
259             window.open(url, '_blank');
260         }
261     }
262
263     showManageDialog(rows: any[]) {
264         const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
265         if (holdIds.length > 0) {
266             this.manageDialog.holdIds = holdIds;
267             this.manageDialog.open({size: 'lg'}).then(
268                 rowsModified => {
269                     if (rowsModified) {
270                         this.holdsGrid.reload();
271                     }
272                 },
273                 dismissed => {}
274             );
275         }
276     }
277
278     showTransferDialog(rows: any[]) {
279         const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
280         if (holdIds.length > 0) {
281             this.transferDialog.holdIds = holdIds;
282             this.transferDialog.open({}).then(
283                 rowsModified => {
284                     if (rowsModified) {
285                         this.holdsGrid.reload();
286                     }
287                 },
288                 dismissed => {}
289             );
290         }
291     }
292
293     async showMarkDamagedDialog(rows: any[]) {
294         const copyIds = rows.map(r => r.cp_id).filter(id => Boolean(id));
295         if (copyIds.length === 0) { return; }
296
297         let rowsModified = false;
298
299         const markNext = async(ids: number[]) => {
300             if (ids.length === 0) {
301                 return Promise.resolve();
302             }
303
304             this.markDamagedDialog.copyId = ids.pop();
305             this.markDamagedDialog.open({size: 'lg'}).then(
306                 ok => {
307                     if (ok) { rowsModified = true; }
308                     return markNext(ids);
309                 },
310                 dismiss => markNext(ids)
311             );
312         };
313
314         await markNext(copyIds);
315         if (rowsModified) {
316             this.holdsGrid.reload();
317         }
318     }
319
320     showMarkMissingDialog(rows: any[]) {
321         const copyIds = rows.map(r => r.cp_id).filter(id => Boolean(id));
322         if (copyIds.length > 0) {
323             this.markMissingDialog.copyIds = copyIds;
324             this.markMissingDialog.open({}).then(
325                 rowsModified => {
326                     if (rowsModified) {
327                         this.holdsGrid.reload();
328                     }
329                 },
330                 dismissed => {} // avoid console errors
331             );
332         }
333     }
334
335     showRetargetDialog(rows: any[]) {
336         const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
337         if (holdIds.length > 0) {
338             this.retargetDialog.holdIds = holdIds;
339             this.retargetDialog.open({}).then(
340                 rowsModified => {
341                     if (rowsModified) {
342                         this.holdsGrid.reload();
343                     }
344                 },
345                 dismissed => {}
346             );
347         }
348     }
349
350     showCancelDialog(rows: any[]) {
351         const holdIds = rows.map(r => r.id).filter(id => Boolean(id));
352         if (holdIds.length > 0) {
353             this.cancelDialog.holdIds = holdIds;
354             this.cancelDialog.open({}).then(
355                 rowsModified => {
356                     if (rowsModified) {
357                         this.holdsGrid.reload();
358                     }
359                 },
360                 dismissed => {}
361             );
362         }
363     }
364 }
365
366