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';
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 OnChanges, OnInit {
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;
38 @Output() onPickup = new EventEmitter<IdlObject>();
39 @Output() onReturn = new EventEmitter<IdlObject>();
41 gridSource: GridDataSource;
42 patronBarcode: string;
43 numRowsSelected: number;
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;
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;
64 handleRowActivate: (row: IdlObject) => void;
65 redirectToCreate: () => void;
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;
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()};
125 } else if ('capturedToday' === this.status) {
126 where['capture_time'] = {'between': [moment().startOf('day').toISOString(),
127 moment().add(1, 'day').startOf('day').toISOString()]};
130 where['return_time'] = null;
133 orderBy.bresv = sort[0].name + ' ' + sort[0].dir;
135 return this.pcrud.search('bresv', where, {
138 offset: pager.offset,
140 flesh_fields: {'bresv' : [
141 'usr', 'capture_staff', 'target_resource', 'target_resource_type', 'current_resource', 'request_lib', 'pickup_lib'
143 }).pipe(mergeMap((row) => this.enrichRow$(row)));
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()));
153 editOneThing(idlThings.shift()); };
155 this.cancelSelected = (reservations: IdlObject[]) => {
156 this.cancelReservationDialog.open(reservations.map(reservation => reservation.id()));
159 this.viewByResource = (reservations: IdlObject[]) => {
160 this.actions.manageReservationsByResource(reservations[0].current_resource().barcode());
163 this.viewByPatron = (reservations: IdlObject[]) => {
164 const patronIds = reservations.map(reservation => reservation.usr().id());
165 this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_patron', patronIds[0]]);
168 this.viewItemStatus = (reservations: IdlObject[]) => {
169 this.actions.viewItemStatus(reservations[0].current_resource().barcode());
172 this.viewPatronRecord = (reservations: IdlObject[]) => {
173 const patronIds = reservations.map(reservation => reservation.usr().id());
174 window.open('/eg/staff/circ/patron/' + patronIds[0] + '/checkout');
177 this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
178 this.notOnePatronSelected = (rows: IdlObject[]) => this.actions.notOneUniqueSelected(rows.map(row => row.usr().id()));
179 this.notOneResourceSelected = (rows: IdlObject[]) => {
180 return this.actions.notOneUniqueSelected(
181 rows.map(row => { if (row.current_resource()) { return row.current_resource().id(); }}));
183 this.notOneCatalogedItemSelected = (rows: IdlObject[]) => {
184 return this.actions.notOneUniqueSelected(
185 rows.filter(row => (row.current_resource() && 't' === row.target_resource_type().catalog_item()))
186 .map(row => row.current_resource().id())
189 this.cancelNotAppropriate = (rows: IdlObject[]) =>
190 (this.noSelectedRows(rows) || ['pickedUp', 'returnReady', 'returnedToday'].includes(this.status));
191 this.pickupNotAppropriate = (rows: IdlObject[]) =>
192 (this.noSelectedRows(rows) || !('pickupReady' === this.status || 'capturedToday' === this.status));
193 this.editNotAppropriate = (rows: IdlObject[]) => (this.noSelectedRows(rows) || ('returnedToday' === this.status));
194 this.reprintNotAppropriate = (rows: IdlObject[]) => {
195 if (this.noSelectedRows(rows)) {
197 } else if ('capturedToday' === this.status) {
199 } else if (rows.filter(row => !(row.capture_time())).length) { // If any of the rows have not been captured
204 this.returnNotAppropriate = (rows: IdlObject[]) => {
205 if (this.noSelectedRows(rows)) {
207 } else if (this.status && ('pickupReady' === this.status || 'capturedToday' === this.status)) {
210 rows.forEach(row => {
211 if ((null == row.pickup_time()) || row.return_time()) { return true; }
217 this.pickupSelected = (reservations: IdlObject[]) => {
218 const pickupOne = (thing: IdlObject) => {
219 if (!thing) { return; }
220 this.pickupResource(thing).subscribe(
221 () => pickupOne(reservations.shift()));
223 pickupOne(reservations.shift());
226 this.returnSelected = (reservations: IdlObject[]) => {
227 const returnOne = (thing: IdlObject) => {
228 if (!thing) { return; }
229 this.returnResource(thing).subscribe(
230 () => returnOne(reservations.shift()));
232 returnOne(reservations.shift());
235 this.reprintCaptureSlip = (reservations: IdlObject[]) => {
236 this.actions.reprintCaptureSlip(reservations.map((r) => r.id())).subscribe();
239 this.pickupResource = (reservation: IdlObject) => {
240 return this.net.request(
242 'open-ils.circ.reservation.pickup',
244 {'patron_barcode': reservation.usr().card().barcode(), 'reservation': reservation})
247 this.onPickup.emit(reservation);
248 this.grid.reload(); },
252 this.returnResource = (reservation: IdlObject) => {
253 return this.net.request(
255 'open-ils.circ.reservation.return',
257 {'patron_barcode': this.patronBarcode, 'reservation': reservation})
260 this.onReturn.emit(reservation);
266 this.listReadOnlyFields = () => {
267 let list = 'usr,xact_start,request_time,capture_time,pickup_time,return_time,capture_staff,target_resource_type,' +
268 'email_notify,current_resource,target_resource,unrecovered,request_lib,pickup_lib,fine_interval,fine_amount,max_fine';
269 if (this.status && ('pickupReady' !== this.status)) { list = list + ',start_time'; }
270 if (this.status && ('returnedToday' === this.status)) { list = list + ',end_time'; }
274 this.handleRowActivate = (row: IdlObject) => {
276 if ('returnReady' === this.status) {
277 this.returnResource(row).subscribe();
278 } else if ('pickupReady' === this.status) {
279 this.pickupResource(row).subscribe();
280 } else if ('returnedToday' === this.status) {
281 this.toast.warning('Cannot edit this reservation');
283 this.showEditDialog(row);
286 this.showEditDialog(row);
290 this.redirectToCreate = () => {
291 this.router.navigate(['/staff', 'booking', 'create_reservation']);
295 ngOnChanges() { this.reloadGrid(); }
297 reloadGrid() { this.grid.reload(); }
299 enrichRow$ = (row: IdlObject): Observable<IdlObject> => {
300 return from(this.org.settings('lib.timezone', row.pickup_lib().id())).pipe(
302 row['length'] = moment(row['end_time']()).from(moment(row['start_time']()), true);
303 row['timezone'] = tz['lib.timezone'];
309 showEditDialog(idlThing: IdlObject) {
310 this.editDialog.recordId = idlThing.id();
311 this.editDialog.timezone = idlThing['timezone'];
312 return new Promise((resolve, reject) => {
313 this.editDialog.open({size: 'lg'}).subscribe(
315 this.toast.success('Reservation successfully updated'); // TODO: needs i18n, pluralization
324 filterByResourceBarcode(barcode: string) {
325 this.router.navigate(['/staff', 'booking', 'manage_reservations', 'by_resource', barcode]);
328 momentizeIsoString(isoString: string, timezone: string): moment.Moment {
329 return this.format.momentizeIsoString(isoString, timezone);