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';
20 import * as Moment from 'moment-timezone';
22 // A filterable grid of reservations used in various booking interfaces
25 selector: 'eg-reservations-grid',
26 templateUrl: './reservations-grid.component.html',
28 export class ReservationsGridComponent implements OnInit {
30 @Input() patron: number;
31 @Input() resourceBarcode: string;
32 @Input() resourceType: number;
33 @Input() pickupLibIds: number[];
34 @Input() status: 'pickupReady' | 'pickedUp' | 'returnReady' | 'returnedToday';
35 @Input() persistSuffix: string;
36 @Input() onlyCaptured = false;
38 @Output() onPickup = new EventEmitter<IdlObject>();
39 @Output() onReturn = new EventEmitter<IdlObject>();
41 gridSource: GridDataSource;
42 patronBarcode: string;
43 numRowsSelected: number;
45 @ViewChild('grid') grid: GridComponent;
46 @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
47 @ViewChild('confirmCancelReservationDialog')
48 private cancelReservationDialog: CancelReservationDialogComponent;
49 @ViewChild('noTimezoneSetDialog') noTimezoneSetDialog: NoTimezoneSetComponent;
51 editSelected: (rows: IdlObject[]) => void;
52 pickupSelected: (rows: IdlObject[]) => void;
53 pickupResource: (rows: IdlObject) => Observable<any>;
54 returnSelected: (rows: IdlObject[]) => void;
55 returnResource: (rows: IdlObject) => Observable<any>;
56 cancelSelected: (rows: IdlObject[]) => void;
57 viewByPatron: (rows: IdlObject[]) => void;
58 viewByResource: (rows: IdlObject[]) => void;
59 viewItemStatus: (rows: IdlObject[]) => void;
60 viewPatronRecord: (rows: IdlObject[]) => void;
61 listReadOnlyFields: () => string;
63 handleRowActivate: (row: IdlObject) => void;
64 redirectToCreate: () => void;
66 reloadGrid: () => 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 editNotAppropriate: (rows: IdlObject[]) => boolean;
75 returnNotAppropriate: (rows: IdlObject[]) => boolean;
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,
91 if (!(this.format.wsOrgTimezone)) {
92 this.noTimezoneSetDialog.open();
95 this.gridSource = new GridDataSource();
97 this.gridSource.getRows = (pager: Pager, sort: any[]): Observable<IdlObject> => {
98 const orderBy: any = {};
100 'usr' : (this.patron ? this.patron : {'>' : 0}),
101 'target_resource_type' : (this.resourceType ? this.resourceType : {'>' : 0}),
102 'cancel_time' : null,
103 'xact_finish' : null,
105 if (this.resourceBarcode) {
106 where['current_resource'] = {'in':
107 {'from': 'brsrc', 'select': {'brsrc': ['id']}, 'where': {'barcode': this.resourceBarcode}}};
109 if (this.pickupLibIds) {
110 where['pickup_lib'] = this.pickupLibIds;
112 if (this.onlyCaptured) {
113 where['capture_time'] = {'!=': null};
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()};
127 where['return_time'] = null;
130 orderBy.bresv = sort[0].name + ' ' + sort[0].dir;
132 return this.pcrud.search('bresv', where, {
135 offset: pager.offset,
137 flesh_fields: {'bresv' : [
138 'usr', 'capture_staff', 'target_resource', 'target_resource_type', 'current_resource', 'request_lib', 'pickup_lib'
140 }).pipe(mergeMap((row) => this.enrichRow$(row)));
143 this.editDialog.mode = 'update';
144 this.editSelected = (idlThings: IdlObject[]) => {
145 const editOneThing = (thing: IdlObject) => {
146 if (!thing) { return; }
147 this.showEditDialog(thing).then(
148 () => editOneThing(idlThings.shift()));
150 editOneThing(idlThings.shift()); };
152 this.cancelSelected = (reservations: IdlObject[]) => {
153 this.cancelReservationDialog.open(reservations.map(reservation => reservation.id()));
156 this.viewByResource = (reservations: IdlObject[]) => {
157 this.actions.manageReservationsByResource(reservations[0].current_resource().barcode());
160 this.viewByPatron = (reservations: IdlObject[]) => {
161 const patronIds = reservations.map(reservation => reservation.usr().id());
162 this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_patron', patronIds[0]]);
165 this.viewItemStatus = (reservations: IdlObject[]) => {
166 this.actions.viewItemStatus(reservations[0].current_resource().barcode());
169 this.viewPatronRecord = (reservations: IdlObject[]) => {
170 const patronIds = reservations.map(reservation => reservation.usr().id());
171 window.open('/eg/staff/circ/patron/' + patronIds[0] + '/checkout');
174 this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
175 this.notOnePatronSelected = (rows: IdlObject[]) => this.actions.notOneUniqueSelected(rows.map(row => row.usr().id()));
176 this.notOneResourceSelected = (rows: IdlObject[]) => {
177 return this.actions.notOneUniqueSelected(
178 rows.map(row => { if (row.current_resource()) { return row.current_resource().id(); }}));
180 this.notOneCatalogedItemSelected = (rows: IdlObject[]) => {
181 return this.actions.notOneUniqueSelected(
182 rows.filter(row => (row.current_resource() && 't' === row.target_resource_type().catalog_item()))
183 .map(row => row.current_resource().id())
186 this.cancelNotAppropriate = (rows: IdlObject[]) =>
187 (this.noSelectedRows(rows) || ['pickedUp', 'returnReady', 'returnedToday'].includes(this.status));
188 this.pickupNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('pickupReady' !== this.status));
189 this.editNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('returnedToday' === this.status));
190 this.returnNotAppropriate = (rows: IdlObject[]) => {
191 if (this.noSelectedRows(rows)) {
193 } else if (this.status && ('pickupReady' === this.status)) {
196 rows.forEach(row => {
197 if ((null == row.pickup_time()) || row.return_time()) { return true; }
203 this.reloadGrid = () => { this.grid.reload(); };
205 this.pickupSelected = (reservations: IdlObject[]) => {
206 const pickupOne = (thing: IdlObject) => {
207 if (!thing) { return; }
208 this.pickupResource(thing).subscribe(
209 () => pickupOne(reservations.shift()));
211 pickupOne(reservations.shift());
214 this.returnSelected = (reservations: IdlObject[]) => {
215 const returnOne = (thing: IdlObject) => {
216 if (!thing) { return; }
217 this.returnResource(thing).subscribe(
218 () => returnOne(reservations.shift()));
220 returnOne(reservations.shift());
223 this.pickupResource = (reservation: IdlObject) => {
224 return this.net.request(
226 'open-ils.circ.reservation.pickup',
228 {'patron_barcode': reservation.usr().card().barcode(), 'reservation': reservation})
231 this.onPickup.emit(reservation);
232 this.grid.reload(); },
236 this.returnResource = (reservation: IdlObject) => {
237 return this.net.request(
239 'open-ils.circ.reservation.return',
241 {'patron_barcode': this.patronBarcode, 'reservation': reservation})
244 this.onReturn.emit(reservation);
250 this.listReadOnlyFields = () => {
251 let list = 'usr,xact_start,request_time,capture_time,pickup_time,return_time,capture_staff,target_resource_type,' +
252 'email_notify,current_resource,target_resource,unrecovered,request_lib,pickup_lib,fine_interval,fine_amount,max_fine';
253 if (this.status && ('pickupReady' !== this.status)) { list = list + ',start_time'; }
254 if (this.status && ('returnedToday' === this.status)) { list = list + ',end_time'; }
258 this.handleRowActivate = (row: IdlObject) => {
260 if ('returnReady' === this.status) {
261 this.returnResource(row).subscribe();
262 } else if ('pickupReady' === this.status) {
263 this.pickupResource(row).subscribe();
264 } else if ('returnedToday' === this.status) {
265 this.toast.warning('Cannot edit this reservation');
267 this.showEditDialog(row);
270 this.showEditDialog(row);
274 this.redirectToCreate = () => {
275 this.router.navigate(['/staff', 'booking', 'create_reservation']);
279 enrichRow$ = (row: IdlObject): Observable<IdlObject> => {
280 return from(this.org.settings('lib.timezone', row.pickup_lib().id())).pipe(
282 row['length'] = Moment(row['end_time']()).from(Moment(row['start_time']()), true);
283 row['timezone'] = tz['lib.timezone'];
289 showEditDialog(idlThing: IdlObject) {
290 this.editDialog.recordId = idlThing.id();
291 this.editDialog.timezone = idlThing['timezone'];
292 return new Promise((resolve, reject) => {
293 this.editDialog.open({size: 'lg'}).subscribe(
295 this.toast.success('Reservation successfully updated'); // TODO: needs i18n, pluralization
304 filterByResourceBarcode(barcode: string) {
305 this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource', barcode]);
308 momentizeIsoString(isoString: string, timezone: string): Moment {
309 return this.format.momentizeIsoString(isoString, timezone);