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