LP1816475: Booking module refresh
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / booking / schedule-grid.service.ts
1 import {Injectable} from '@angular/core';
2 import {Observable, of} from 'rxjs';
3 import {switchMap} from 'rxjs/operators';
4 import {NgbTimeStruct} from '@ng-bootstrap/ng-bootstrap';
5 import {AuthService} from '@eg/core/auth.service';
6 import {IdlObject} from '@eg/core/idl.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {GridRowFlairEntry} from '@eg/share/grid/grid';
9 import {DateRange} from '@eg/share/daterange-select/daterange-select.component';
10
11 import * as Moment from 'moment-timezone';
12
13 export interface ReservationPatron {
14   patronId: number;
15   patronLabel: string;
16   reservationId: number;
17 }
18
19 export interface ScheduleRow {
20     time: Moment;
21     [key: string]: ReservationPatron[];
22 }
23
24 // Various methods that fetch data for and process the schedule of reservations
25
26 @Injectable({providedIn: 'root'})
27 export class ScheduleGridService {
28
29     constructor(
30         private auth: AuthService,
31         private pcrud: PcrudService,
32     ) {
33     }
34     hoursOfOperation = (date: Date): Observable<{startOfDay: NgbTimeStruct, endOfDay: NgbTimeStruct}> => {
35         const defaultStartHour = 9;
36         const defaultEndHour = 17;
37         return this.pcrud.retrieve('aouhoo', this.auth.user().ws_ou())
38             .pipe(switchMap((hours) => {
39                 const startArray = hours[this.evergreenStyleDow(date) + '_open']().split(':');
40                 const endArray = hours[this.evergreenStyleDow(date) + '_close']().split(':');
41                 return of({
42                     startOfDay: {
43                         hour: ('00' === startArray[0]) ? defaultStartHour : +startArray[0],
44                         minute: +startArray[1],
45                         second: 0},
46                     endOfDay: {
47                         hour: ('00' === endArray[0]) ? defaultEndHour : +endArray[0],
48                         minute: +endArray[1],
49                         second: 0}
50                 });
51             }));
52     }
53
54     resourceAvailabilityIcon = (row: ScheduleRow, numResources: number): GridRowFlairEntry => {
55         let icon = {icon: 'event_busy', title: 'All resources are reserved at this time'};
56         let busyColumns = 0;
57         for (const key in row) {
58             if (row[key] instanceof Array && row[key].length) {
59                 busyColumns += 1;
60             }
61         }
62         if (busyColumns < numResources) {
63             icon = {icon: 'event_available', title: 'Resources are available at this time'};
64         }
65         return icon;
66     }
67
68     fetchRelevantResources = (resourceTypeId: number, owningLibraries: number[], selectedAttributes: number[]): Observable<IdlObject> => {
69         const where = {
70             type: resourceTypeId,
71             owner: owningLibraries,
72         };
73
74         if (selectedAttributes.length) {
75             where['id'] = {'in':
76                 {'from': 'bram', 'select': {'bram': ['resource']},
77                 'where': {'value':  selectedAttributes}}};
78         }
79         return this.pcrud.search('brsrc', where, {
80             order_by: 'barcode ASC',
81             flesh: 1,
82             flesh_fields: {'brsrc': ['attr_maps']},
83         });
84     }
85
86     momentizeDateRange = (range: DateRange, timezone: string): {startTime: Moment, endTime: Moment} => {
87         return {
88             startTime: Moment.tz([
89                 range.fromDate.year,
90                 range.fromDate.month - 1,
91                 range.fromDate.day],
92                 timezone),
93             endTime: Moment.tz([
94                 range.toDate.year,
95                 range.toDate.month - 1,
96                 range.toDate.day + 1],
97                 timezone)
98         };
99     }
100     momentizeDay = (date: Date, start: NgbTimeStruct, end: NgbTimeStruct, timezone: string): {startTime: Moment, endTime: Moment} => {
101         return {
102             startTime: Moment.tz([
103                 date.getFullYear(),
104                 date.getMonth(),
105                 date.getDate(),
106                 start.hour,
107                 start.minute],
108                 timezone),
109             endTime: Moment.tz([
110                 date.getFullYear(),
111                 date.getMonth(),
112                 date.getDate(),
113                 end.hour,
114                 end.minute],
115                 timezone)
116         };
117     }
118
119     createBasicSchedule = (range: {startTime: Moment, endTime: Moment}, granularity: number): ScheduleRow[] => {
120         const currentTime = range.startTime.clone();
121         const schedule = [];
122         while (currentTime < range.endTime) {
123             schedule.push({'time': currentTime.clone()});
124             currentTime.add(granularity, 'minutes');
125         }
126         return schedule;
127     }
128
129     fetchReservations = (range: {startTime: Moment, endTime: Moment}, resourceIds: number[]): Observable<IdlObject> => {
130         return this.pcrud.search('bresv', {
131             '-or': {'target_resource': resourceIds, 'current_resource': resourceIds},
132             'end_time': {'>': range.startTime.toISOString()},
133             'start_time': {'<': range.endTime.toISOString()},
134             'return_time': null,
135             'cancel_time': null },
136             {'flesh': 1, 'flesh_fields': {'bresv': ['current_resource', 'usr']}});
137     }
138
139     addReservationToSchedule = (reservation: IdlObject, schedule: ScheduleRow[], granularity: number, timezone: string): ScheduleRow[] => {
140         for (let index = 0; index < schedule.length; index++) {
141             const start = schedule[index].time;
142             const end = (index + 1 < schedule.length) ?
143                 schedule[index + 1].time :
144                 schedule[index].time.clone().add(granularity, 'minutes');
145             if ((Moment.tz(reservation.start_time(), timezone).isBefore(end)) &&
146                 (Moment.tz(reservation.end_time(), timezone).isAfter(start))) {
147                 if (!schedule[index][reservation.current_resource().barcode()]) {
148                     schedule[index][reservation.current_resource().barcode()] = [];
149                 }
150                 if (schedule[index][reservation.current_resource().barcode()]
151                     .findIndex(patron => patron.patronId === reservation.usr().id()) === -1) {
152                     schedule[index][reservation.current_resource().barcode()].push(
153                         {'patronLabel': reservation.usr().usrname(),
154                         'patronId': reservation.usr().id(),
155                         'reservationId': reservation.id()});
156                 }
157             }
158
159         }
160         return schedule;
161
162     }
163
164     // Evergreen uses its own day of week style, where dow_0 = Monday and dow_6 = Sunday
165     private evergreenStyleDow = (original: Date): string => {
166         const daysInAWeek = 7;
167         const offset = 6;
168         return 'dow_' + (original.getDay() + offset) % daysInAWeek;
169     }
170
171
172 }
173