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