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';
21 import * as moment from 'moment-timezone';
23 // A filterable grid of reservations used in various booking interfaces
26 selector: 'eg-reservations-grid',
27 templateUrl: './reservations-grid.component.html',
29 export class ReservationsGridComponent implements OnChanges, OnInit {
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;
39 @Output() pickedUpResource = new EventEmitter<IdlObject>();
40 @Output() returnedResource = new EventEmitter<IdlObject>();
42 gridSource: GridDataSource;
43 patronBarcode: string;
44 numRowsSelected: number;
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;
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;
65 handleRowActivate: (row: IdlObject) => void;
66 redirectToCreate: () => void;
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;
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,
93 if (!(this.format.wsOrgTimezone)) {
94 this.noTimezoneSetDialog.open();
97 this.gridSource = new GridDataSource();
99 this.gridSource.getRows = (pager: Pager, sort: any[]): Observable<IdlObject> => {
100 const orderBy: any = {};
102 'usr' : (this.patron ? this.patron : {'>' : 0}),
103 'target_resource_type' : (this.resourceType ? this.resourceType : {'>' : 0}),
104 'cancel_time' : null,
105 'xact_finish' : null,
107 if (this.resourceBarcode) {
108 where['current_resource'] = {'in':
109 {'from': 'brsrc', 'select': {'brsrc': ['id']}, 'where': {'barcode': this.resourceBarcode}}};
111 if (this.pickupLibIds) {
112 where['pickup_lib'] = this.pickupLibIds;
114 if (this.onlyCaptured) {
115 where['capture_time'] = {'!=': null};
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()]};
132 where['return_time'] = null;
135 orderBy.bresv = sort[0].name + ' ' + sort[0].dir;
137 return this.pcrud.search('bresv', where, {
140 offset: pager.offset,
142 flesh_fields: {'bresv' : [
143 'usr', 'capture_staff', 'target_resource', 'target_resource_type', 'current_resource', 'request_lib', 'pickup_lib'
145 }).pipe(mergeMap((row) => this.enrichRow$(row)));
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()));
155 editOneThing(idlThings.shift()); };
157 this.cancelSelected = (reservations: IdlObject[]) => {
158 this.cancelReservationDialog.open(reservations.map(reservation => reservation.id()));
161 this.viewByResource = (reservations: IdlObject[]) => {
162 this.actions.manageReservationsByResource(reservations[0].current_resource().barcode());
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]]);
170 this.viewItemStatus = (reservations: IdlObject[]) => {
171 this.actions.viewItemStatus(reservations[0].current_resource().barcode());
174 this.viewPatronRecord = (reservations: IdlObject[]) => {
175 const patronIds = reservations.map(reservation => reservation.usr().id());
177 this.ngLocation.prepareExternalUrl(
178 '/staff/circ/patron/' + patronIds[0] + '/checkout'
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(); }}));
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())
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)) {
203 } else if ('capturedToday' === this.status) {
205 } else if (rows.filter(row => !(row.capture_time())).length) { // If any of the rows have not been captured
210 this.returnNotAppropriate = (rows: IdlObject[]) => {
211 if (this.noSelectedRows(rows)) {
213 } else if (this.status && ('pickupReady' === this.status || 'capturedToday' === this.status)) {
216 rows.forEach(row => {
217 if ((null == row.pickup_time()) || row.return_time()) { return true; }
223 this.pickupSelected = (reservations: IdlObject[]) => {
224 const pickupOne = (thing: IdlObject) => {
225 if (!thing) { return; }
226 this.pickupResource(thing).subscribe(
227 () => pickupOne(reservations.shift()));
229 pickupOne(reservations.shift());
232 this.returnSelected = (reservations: IdlObject[]) => {
233 const returnOne = (thing: IdlObject) => {
234 if (!thing) { return; }
235 this.returnResource(thing).subscribe(
236 () => returnOne(reservations.shift()));
238 returnOne(reservations.shift());
241 this.reprintCaptureSlip = (reservations: IdlObject[]) => {
242 this.actions.reprintCaptureSlip(reservations.map((r) => r.id())).subscribe();
245 this.pickupResource = (reservation: IdlObject) => {
246 return this.net.request(
248 'open-ils.circ.reservation.pickup',
250 {'patron_barcode': reservation.usr().card().barcode(), 'reservation': reservation})
253 this.pickedUpResource.emit(reservation);
254 this.grid.reload(); },
258 this.returnResource = (reservation: IdlObject) => {
259 return this.net.request(
261 'open-ils.circ.reservation.return',
263 {'patron_barcode': this.patronBarcode, 'reservation': reservation})
266 this.returnedResource.emit(reservation);
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'; }
280 this.handleRowActivate = (row: IdlObject) => {
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');
289 this.showEditDialog(row);
292 this.showEditDialog(row);
296 this.redirectToCreate = () => {
297 this.router.navigate(['/staff', 'booking', 'create_reservation']);
301 ngOnChanges() { this.reloadGrid(); }
303 reloadGrid() { this.grid.reload(); }
305 enrichRow$ = (row: IdlObject): Observable<IdlObject> => {
306 return from(this.org.settings('lib.timezone', row.pickup_lib().id())).pipe(
308 row['length'] = moment(row['end_time']()).from(moment(row['start_time']()), true);
309 row['timezone'] = tz['lib.timezone'];
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(
321 this.toast.success('Reservation successfully updated'); // TODO: needs i18n, pluralization
330 filterByResourceBarcode(barcode: string) {
331 this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource', barcode]);
334 momentizeIsoString(isoString: string, timezone: string): moment.Moment {
335 return this.format.momentizeIsoString(isoString, timezone);