]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/booking/reservations-grid.component.ts
LP 2061136 follow-up: ng lint --fix
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / booking / reservations-grid.component.ts
1 import {Component, EventEmitter, Input, Output, OnChanges, OnInit, ViewChild} from '@angular/core';
2 import {Router} from '@angular/router';
3 import {Observable, from, of} from 'rxjs';
4 import {tap, switchMap, mergeMap} from 'rxjs/operators';
5 import {AuthService} from '@eg/core/auth.service';
6 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
7 import {FormatService} from '@eg/core/format.service';
8 import {GridComponent} from '@eg/share/grid/grid.component';
9 import {GridDataSource} from '@eg/share/grid/grid';
10 import {IdlObject} from '@eg/core/idl.service';
11 import {PcrudService} from '@eg/core/pcrud.service';
12 import {Pager} from '@eg/share/util/pager';
13 import {ToastService} from '@eg/share/toast/toast.service';
14 import {NetService} from '@eg/core/net.service';
15 import {OrgService} from '@eg/core/org.service';
16 import {NoTimezoneSetComponent} from './no-timezone-set.component';
17 import {ReservationActionsService} from './reservation-actions.service';
18 import {CancelReservationDialogComponent} from './cancel-reservation-dialog.component';
19
20 import * as moment from 'moment-timezone';
21
22 // A filterable grid of reservations used in various booking interfaces
23
24 @Component({
25     selector: 'eg-reservations-grid',
26     templateUrl: './reservations-grid.component.html',
27 })
28 export class ReservationsGridComponent implements OnChanges, OnInit {
29
30     @Input() patron: number;
31     @Input() resourceBarcode: string;
32     @Input() resourceType: number;
33     @Input() pickupLibIds: number[];
34     @Input() status: 'capturedToday' | 'pickupReady' | 'pickedUp' | 'returnReady' | 'returnedToday';
35     @Input() persistSuffix: string;
36     @Input() onlyCaptured = false;
37
38     @Output() pickedUpResource = new EventEmitter<IdlObject>();
39     @Output() returnedResource = new EventEmitter<IdlObject>();
40
41     gridSource: GridDataSource;
42     patronBarcode: string;
43     numRowsSelected: number;
44
45     @ViewChild('grid', { static: true }) grid: GridComponent;
46     @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
47     @ViewChild('confirmCancelReservationDialog', { static: true })
48     private cancelReservationDialog: CancelReservationDialogComponent;
49     @ViewChild('noTimezoneSetDialog', { static: true }) noTimezoneSetDialog: NoTimezoneSetComponent;
50
51     editSelected: (rows: IdlObject[]) => void;
52     pickupSelected: (rows: IdlObject[]) => void;
53     pickupResource: (rows: IdlObject) => Observable<any>;
54     reprintCaptureSlip: (rows: IdlObject[]) => void;
55     returnSelected: (rows: IdlObject[]) => void;
56     returnResource: (rows: IdlObject) => Observable<any>;
57     cancelSelected: (rows: IdlObject[]) => void;
58     viewByPatron: (rows: IdlObject[]) => void;
59     viewByResource: (rows: IdlObject[]) => void;
60     viewItemStatus: (rows: IdlObject[]) => void;
61     viewPatronRecord: (rows: IdlObject[]) => void;
62     listReadOnlyFields: () => string;
63
64     handleRowActivate: (row: IdlObject) => void;
65     redirectToCreate: () => void;
66
67     noSelectedRows: (rows: IdlObject[]) => boolean;
68     notOnePatronSelected: (rows: IdlObject[]) => boolean;
69     notOneResourceSelected: (rows: IdlObject[]) => boolean;
70     notOneCatalogedItemSelected: (rows: IdlObject[]) => boolean;
71     cancelNotAppropriate: (rows: IdlObject[]) => boolean;
72     pickupNotAppropriate: (rows: IdlObject[]) => boolean;
73     reprintNotAppropriate: (rows: IdlObject[]) => boolean;
74     editNotAppropriate: (rows: IdlObject[]) => boolean;
75     returnNotAppropriate: (rows: IdlObject[]) => boolean;
76
77     constructor(
78         private auth: AuthService,
79         private format: FormatService,
80         private pcrud: PcrudService,
81         private router: Router,
82         private toast: ToastService,
83         private net: NetService,
84         private org: OrgService,
85         private actions: ReservationActionsService,
86     ) {
87
88     }
89
90     ngOnInit() {
91         if (!(this.format.wsOrgTimezone)) {
92             this.noTimezoneSetDialog.open();
93         }
94
95         this.gridSource = new GridDataSource();
96
97         this.gridSource.getRows = (pager: Pager, sort: any[]): Observable<IdlObject> => {
98             const orderBy: any = {};
99             const where = {
100                 'usr' : (this.patron ? this.patron : {'>' : 0}),
101                 'target_resource_type' : (this.resourceType ? this.resourceType : {'>' : 0}),
102                 'cancel_time' : null,
103                 'xact_finish' : null,
104             };
105             if (this.resourceBarcode) {
106                 where['current_resource'] = {'in':
107                     {'from': 'brsrc', 'select': {'brsrc': ['id']}, 'where': {'barcode': this.resourceBarcode}}};
108             }
109             if (this.pickupLibIds) {
110                 where['pickup_lib'] = this.pickupLibIds;
111             }
112             if (this.onlyCaptured) {
113                 where['capture_time'] = {'!=': null};
114             }
115
116             if (this.status) {
117                 if ('pickupReady' === this.status) {
118                     where['pickup_time'] = null;
119                     where['start_time'] = {'!=': null};
120                 } else if ('pickedUp' === this.status || 'returnReady' === this.status) {
121                     where['pickup_time'] = {'!=': null};
122                     where['return_time'] = null;
123                 } else if ('returnedToday' === this.status) {
124                     where['return_time'] = {'>': moment().startOf('day').toISOString()};
125                 } else if ('capturedToday' === this.status) {
126                     where['capture_time'] = {'between': [moment().startOf('day').toISOString(),
127                         moment().add(1, 'day').startOf('day').toISOString()]};
128                 }
129             } else {
130                 where['return_time'] = null;
131             }
132             if (sort.length) {
133                 orderBy.bresv = sort[0].name + ' ' + sort[0].dir;
134             }
135             return this.pcrud.search('bresv', where,  {
136                 order_by: orderBy,
137                 limit: pager.limit,
138                 offset: pager.offset,
139                 flesh: 2,
140                 flesh_fields: {'bresv' : [
141                     'usr', 'capture_staff', 'target_resource', 'target_resource_type', 'current_resource', 'request_lib', 'pickup_lib'
142                 ], 'au': ['card'] }
143             }).pipe(mergeMap((row) => this.enrichRow$(row)));
144         };
145
146         this.editDialog.mode = 'update';
147         this.editSelected = (idlThings: IdlObject[]) => {
148             const editOneThing = (thing: IdlObject) => {
149                 if (!thing) { return; }
150                 this.showEditDialog(thing).then(
151                     () => editOneThing(idlThings.shift()));
152             };
153             editOneThing(idlThings.shift());
154         };
155
156         this.cancelSelected = (reservations: IdlObject[]) => {
157             this.cancelReservationDialog.open(reservations.map(reservation => reservation.id()));
158         };
159
160         this.viewByResource = (reservations: IdlObject[]) => {
161             this.actions.manageReservationsByResource(reservations[0].current_resource().barcode());
162         };
163
164         this.viewByPatron = (reservations: IdlObject[]) => {
165             const patronIds = reservations.map(reservation => reservation.usr().id());
166             this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_patron', patronIds[0]]);
167         };
168
169         this.viewItemStatus = (reservations: IdlObject[]) => {
170             this.actions.viewItemStatus(reservations[0].current_resource().barcode());
171         };
172
173         this.viewPatronRecord = (reservations: IdlObject[]) => {
174             const patronIds = reservations.map(reservation => reservation.usr().id());
175             window.open('/eg/staff/circ/patron/' + patronIds[0] + '/checkout');
176         };
177
178         this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
179         this.notOnePatronSelected = (rows: IdlObject[]) => this.actions.notOneUniqueSelected(rows.map(row => row.usr().id()));
180         this.notOneResourceSelected = (rows: IdlObject[]) => {
181             return this.actions.notOneUniqueSelected(
182                 rows.map(row => { if (row.current_resource()) { return row.current_resource().id(); }}));
183         };
184         this.notOneCatalogedItemSelected = (rows: IdlObject[]) => {
185             return this.actions.notOneUniqueSelected(
186                 rows.filter(row => (row.current_resource() && 't' === row.target_resource_type().catalog_item()))
187                     .map(row => row.current_resource().id())
188             );
189         };
190         this.cancelNotAppropriate = (rows: IdlObject[]) =>
191             (this.noSelectedRows(rows) || ['pickedUp', 'returnReady', 'returnedToday'].includes(this.status));
192         this.pickupNotAppropriate = (rows: IdlObject[]) =>
193             (this.noSelectedRows(rows) || !('pickupReady' === this.status || 'capturedToday' === this.status));
194         this.editNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('returnedToday' === this.status));
195         this.reprintNotAppropriate = (rows: IdlObject[]) => {
196             if (this.noSelectedRows(rows)) {
197                 return true;
198             } else if ('capturedToday' === this.status) {
199                 return false;
200             } else if (rows.filter(row => !(row.capture_time())).length) { // If any of the rows have not been captured
201                 return true;
202             }
203             return false;
204         };
205         this.returnNotAppropriate = (rows: IdlObject[]) => {
206             if (this.noSelectedRows(rows)) {
207                 return true;
208             } else if (this.status && ('pickupReady' === this.status || 'capturedToday' === this.status)) {
209                 return true;
210             } else {
211                 rows.forEach(row => {
212                     // eslint-disable-next-line eqeqeq
213                     if ((null == row.pickup_time()) || row.return_time()) { return true; }
214                 });
215             }
216             return false;
217         };
218
219         this.pickupSelected = (reservations: IdlObject[]) => {
220             const pickupOne = (thing: IdlObject) => {
221                 if (!thing) { return; }
222                 this.pickupResource(thing).subscribe(
223                     () => pickupOne(reservations.shift()));
224             };
225             pickupOne(reservations.shift());
226         };
227
228         this.returnSelected = (reservations: IdlObject[]) => {
229             const returnOne = (thing: IdlObject) => {
230                 if (!thing) { return; }
231                 this.returnResource(thing).subscribe(
232                     () => returnOne(reservations.shift()));
233             };
234             returnOne(reservations.shift());
235         };
236
237         this.reprintCaptureSlip = (reservations: IdlObject[]) => {
238             this.actions.reprintCaptureSlip(reservations.map((r) => r.id())).subscribe();
239         };
240
241         this.pickupResource = (reservation: IdlObject) => {
242             return this.net.request(
243                 'open-ils.circ',
244                 'open-ils.circ.reservation.pickup',
245                 this.auth.token(),
246                 {'patron_barcode': reservation.usr().card().barcode(), 'reservation': reservation})
247                 .pipe(tap(
248                     () => {
249                         this.pickedUpResource.emit(reservation);
250                         this.grid.reload();
251                     },
252                 ));
253         };
254
255         this.returnResource = (reservation: IdlObject) => {
256             return this.net.request(
257                 'open-ils.circ',
258                 'open-ils.circ.reservation.return',
259                 this.auth.token(),
260                 {'patron_barcode': this.patronBarcode, 'reservation': reservation})
261                 .pipe(tap(
262                     () => {
263                         this.returnedResource.emit(reservation);
264                         this.grid.reload();
265                     },
266                 ));
267         };
268
269         this.listReadOnlyFields = () => {
270             let list = 'usr,xact_start,request_time,capture_time,pickup_time,return_time,capture_staff,target_resource_type,' +
271                 'email_notify,current_resource,target_resource,unrecovered,request_lib,pickup_lib,fine_interval,fine_amount,max_fine';
272             if (this.status && ('pickupReady' !== this.status)) { list = list + ',start_time'; }
273             if (this.status && ('returnedToday' === this.status)) { list = list + ',end_time'; }
274             return list;
275         };
276
277         this.handleRowActivate = (row: IdlObject) => {
278             if (this.status) {
279                 if ('returnReady' === this.status) {
280                     this.returnResource(row).subscribe();
281                 } else if ('pickupReady' === this.status) {
282                     this.pickupResource(row).subscribe();
283                 } else if ('returnedToday' === this.status) {
284                     this.toast.warning('Cannot edit this reservation');
285                 } else {
286                     this.showEditDialog(row);
287                 }
288             } else {
289                 this.showEditDialog(row);
290             }
291         };
292
293         this.redirectToCreate = () => {
294             this.router.navigate(['/staff', 'booking', 'create_reservation']);
295         };
296     }
297
298     ngOnChanges() { this.reloadGrid(); }
299
300     reloadGrid() { this.grid.reload(); }
301
302     enrichRow$ = (row: IdlObject): Observable<IdlObject> => {
303         return from(this.org.settings('lib.timezone', row.pickup_lib().id())).pipe(
304             switchMap((tz) => {
305                 row['length'] = moment(row['end_time']()).from(moment(row['start_time']()), true);
306                 row['timezone'] = tz['lib.timezone'];
307                 return of(row);
308             })
309         );
310     };
311
312     showEditDialog(idlThing: IdlObject) {
313         this.editDialog.recordId = idlThing.id();
314         this.editDialog.timezone = idlThing['timezone'];
315         return new Promise((resolve, reject) => {
316             this.editDialog.open({size: 'lg'}).subscribe(
317                 ok => {
318                     this.toast.success('Reservation successfully updated'); // TODO: needs i18n, pluralization
319                     this.grid.reload();
320                     resolve(ok);
321                 },
322                 (rejection: unknown) => {}
323             );
324         });
325     }
326
327     filterByResourceBarcode(barcode: string) {
328         this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource', barcode]);
329     }
330
331     momentizeIsoString(isoString: string, timezone: string): moment.Moment {
332         return this.format.momentizeIsoString(isoString, timezone);
333     }
334 }
335