LP1806087 Angular catalog Ang7 & lint repairs
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / catalog / hold / hold.component.ts
1 import {Component, OnInit, Input, ViewChild, Renderer2} from '@angular/core';
2 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
3 import {Observable} from 'rxjs';
4 import {tap} from 'rxjs/operators';
5 import {EventService} from '@eg/core/event.service';
6 import {NetService} from '@eg/core/net.service';
7 import {AuthService} from '@eg/core/auth.service';
8 import {PcrudService} from '@eg/core/pcrud.service';
9 import {PermService} from '@eg/core/perm.service';
10 import {IdlObject} from '@eg/core/idl.service';
11 import {OrgService} from '@eg/core/org.service';
12 import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
13 import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
14 import {CatalogService} from '@eg/share/catalog/catalog.service';
15 import {StaffCatalogService} from '../catalog.service';
16 import {HoldService, HoldRequest,
17     HoldRequestTarget} from '@eg/staff/share/hold.service';
18 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
19
20 class HoldContext {
21     holdMeta: HoldRequestTarget;
22     holdTarget: number;
23     lastRequest: HoldRequest;
24     canOverride?: boolean;
25     processing: boolean;
26     selectedFormats: any;
27
28     constructor(target: number) {
29         this.holdTarget = target;
30         this.processing = false;
31         this.selectedFormats = {
32            // code => selected-boolean
33            formats: {},
34            langs: {}
35         };
36     }
37 }
38
39 @Component({
40   templateUrl: 'hold.component.html'
41 })
42 export class HoldComponent implements OnInit {
43
44     holdType: string;
45     holdTargets: number[];
46     user: IdlObject; //
47     userBarcode: string;
48     requestor: IdlObject;
49     holdFor: string;
50     pickupLib: number;
51     notifyEmail: boolean;
52     notifyPhone: boolean;
53     phoneValue: string;
54     notifySms: boolean;
55     smsValue: string;
56     smsCarrier: string;
57     suspend: boolean;
58     activeDate: string;
59
60     holdContexts: HoldContext[];
61     recordSummaries: BibRecordSummary[];
62
63     currentUserBarcode: string;
64     smsCarriers: ComboboxEntry[];
65
66     smsEnabled: boolean;
67     placeHoldsClicked: boolean;
68
69     constructor(
70         private router: Router,
71         private route: ActivatedRoute,
72         private renderer: Renderer2,
73         private evt: EventService,
74         private net: NetService,
75         private org: OrgService,
76         private auth: AuthService,
77         private pcrud: PcrudService,
78         private bib: BibRecordService,
79         private cat: CatalogService,
80         private staffCat: StaffCatalogService,
81         private holds: HoldService,
82         private perm: PermService
83     ) {
84         this.holdContexts = [];
85         this.smsCarriers = [];
86     }
87
88     ngOnInit() {
89
90         this.holdType = this.route.snapshot.params['type'];
91         this.holdTargets = this.route.snapshot.queryParams['target'];
92
93         if (!Array.isArray(this.holdTargets)) {
94             this.holdTargets = [this.holdTargets];
95         }
96
97         this.holdTargets = this.holdTargets.map(t => Number(t));
98         this.holdFor = 'patron';
99         this.requestor = this.auth.user();
100         this.pickupLib = this.auth.user().ws_ou();
101
102         this.holdContexts = this.holdTargets.map(target => {
103             const ctx = new HoldContext(target);
104             return ctx;
105         });
106
107         this.getTargetMeta();
108
109         this.org.settings('sms.enable').then(sets => {
110             this.smsEnabled = sets['sms.enable'];
111             if (!this.smsEnabled) { return; }
112
113             this.pcrud.search('csc', {active: 't'}, {order_by: {csc: 'name'}})
114             .subscribe(carrier => {
115                 this.smsCarriers.push({
116                     id: carrier.id(),
117                     label: carrier.name()
118                 });
119             });
120         });
121
122         setTimeout(() => // Focus barcode input
123             this.renderer.selectRootElement('#patron-barcode').focus());
124     }
125
126     // Load the bib, call number, copy, etc. data associated with each target.
127     getTargetMeta() {
128         this.holds.getHoldTargetMeta(this.holdType, this.holdTargets)
129         .subscribe(meta => {
130             this.holdContexts.filter(ctx => ctx.holdTarget === meta.target)
131             .forEach(ctx => {
132                 ctx.holdMeta = meta;
133                 this.mrFiltersToSelectors(ctx);
134             });
135         });
136     }
137
138     // By default, all metarecord filters options are enabled.
139     mrFiltersToSelectors(ctx: HoldContext) {
140         if (this.holdType !== 'M') { return; }
141
142         const meta = ctx.holdMeta;
143         if (meta.metarecord_filters) {
144             if (meta.metarecord_filters.formats) {
145                 meta.metarecord_filters.formats.forEach(
146                     ccvm => ctx.selectedFormats.formats[ccvm.code()] = true);
147             }
148             if (meta.metarecord_filters.langs) {
149                 meta.metarecord_filters.langs.forEach(
150                     ccvm => ctx.selectedFormats.langs[ccvm.code()] = true);
151             }
152         }
153     }
154
155     // Map the selected metarecord filters optoins to a JSON-encoded
156     // list of attr filters as required by the API.
157     // Compiles a blob of
158     // {target: JSON({"0": [{_attr: ctype, _val: code}, ...], "1": [...]})}
159     // TODO: this should live in the hold service, not in the UI code.
160     mrSelectorsToFilters(ctx: HoldContext): {[target: number]: string} {
161
162         const meta = ctx.holdMeta;
163         const slf = ctx.selectedFormats;
164         const result: any = {};
165
166         const formats = Object.keys(slf.formats)
167             .filter(code => Boolean(slf.formats[code])); // user-selected
168
169         const langs = Object.keys(slf.langs)
170             .filter(code => Boolean(slf.langs[code])); // user-selected
171
172         const compiled: any = {};
173
174         if (formats.length > 0) {
175             compiled['0'] = [];
176             formats.forEach(code => {
177                 const ccvm = meta.metarecord_filters.formats.filter(
178                     format => format.code() === code)[0];
179                 compiled['0'].push({
180                     _attr: ccvm.ctype(),
181                     _val: ccvm.code()
182                 });
183             });
184         }
185
186         if (langs.length > 0) {
187             compiled['1'] = [];
188             langs.forEach(code => {
189                 const ccvm = meta.metarecord_filters.langs.filter(
190                     format => format.code() === code)[0];
191                 compiled['1'].push({
192                     _attr: ccvm.ctype(),
193                     _val: ccvm.code()
194                 });
195             });
196         }
197
198         if (Object.keys(compiled).length > 0) {
199             const res = {};
200             res[ctx.holdTarget] = JSON.stringify(compiled);
201             return res;
202         }
203
204         return null;
205     }
206
207     holdForChanged() {
208         this.user = null;
209
210         if (this.holdFor === 'patron') {
211             if (this.userBarcode) {
212                 this.userBarcodeChanged();
213             }
214         } else {
215             // To bypass the dupe check.
216             this.currentUserBarcode = '_' + this.requestor.id();
217             this.getUser(this.requestor.id());
218         }
219     }
220
221     activeDateSelected(dateStr: string) {
222         this.activeDate = dateStr;
223     }
224
225     userBarcodeChanged() {
226
227         // Avoid simultaneous or duplicate lookups
228         if (this.userBarcode === this.currentUserBarcode) {
229             return;
230         }
231
232         this.resetForm();
233
234         if (!this.userBarcode) {
235             this.user = null;
236             return;
237         }
238
239         this.user = null;
240         this.currentUserBarcode = this.userBarcode;
241
242         this.net.request(
243             'open-ils.actor',
244             'open-ils.actor.get_barcodes',
245             this.auth.token(), this.auth.user().ws_ou(),
246             'actor', this.userBarcode
247         ).subscribe(barcodes => {
248
249             // Use the first successful barcode response.
250             // TODO: What happens when there are multiple responses?
251             // Use for-loop for early exit since we have async
252             // action within the loop.
253             for (let i = 0; i < barcodes.length; i++) {
254                 const bc = barcodes[i];
255                 if (!this.evt.parse(bc)) {
256                     this.getUser(bc.id);
257                     break;
258                 }
259             }
260         });
261     }
262
263     resetForm() {
264         this.notifyEmail = true;
265         this.notifyPhone = true;
266         this.phoneValue = '';
267         this.pickupLib = this.requestor.ws_ou();
268     }
269
270     getUser(id: number) {
271         this.pcrud.retrieve('au', id, {flesh: 1, flesh_fields: {au: ['settings']}})
272         .subscribe(user => {
273             this.user = user;
274             this.applyUserSettings();
275         });
276     }
277
278     applyUserSettings() {
279         if (!this.user || !this.user.settings()) { return; }
280
281         // Start with defaults.
282         this.phoneValue = this.user.day_phone() || this.user.evening_phone();
283
284         // Default to work org if placing holds for staff.
285         if (this.user.id() !== this.requestor.id()) {
286             this.pickupLib = this.user.home_ou();
287         }
288
289         this.user.settings().forEach(setting => {
290             const name = setting.name();
291             const value = setting.value();
292
293             if (value === '' || value === null) { return; }
294
295             switch (name) {
296                 case 'opac.hold_notify':
297                     this.notifyPhone = Boolean(value.match(/phone/));
298                     this.notifyEmail = Boolean(value.match(/email/));
299                     this.notifySms = Boolean(value.match(/sms/));
300                     break;
301
302                 case 'opac.default_pickup_location':
303                     this.pickupLib = value;
304                     break;
305             }
306         });
307
308         if (!this.user.email()) {
309             this.notifyEmail = false;
310         }
311
312         if (!this.phoneValue) {
313             this.notifyPhone = false;
314         }
315     }
316
317     // Attempt hold placement on all targets
318     placeHolds(idx?: number) {
319         if (!idx) { idx = 0; }
320         if (!this.holdTargets[idx]) { return; }
321         this.placeHoldsClicked = true;
322
323         const target = this.holdTargets[idx];
324         const ctx = this.holdContexts.filter(
325             c => c.holdTarget === target)[0];
326
327         this.placeOneHold(ctx).then(() => this.placeHolds(idx + 1));
328     }
329
330     placeOneHold(ctx: HoldContext, override?: boolean): Promise<any> {
331
332         ctx.processing = true;
333         const selectedFormats = this.mrSelectorsToFilters(ctx);
334
335         return this.holds.placeHold({
336             holdTarget: ctx.holdTarget,
337             holdType: this.holdType,
338             recipient: this.user.id(),
339             requestor: this.requestor.id(),
340             pickupLib: this.pickupLib,
341             override: override,
342             notifyEmail: this.notifyEmail, // bool
343             notifyPhone: this.notifyPhone ? this.phoneValue : null,
344             notifySms: this.notifySms ? this.smsValue : null,
345             smsCarrier: this.notifySms ? this.smsCarrier : null,
346             thawDate: this.suspend ? this.activeDate : null,
347             frozen: this.suspend,
348             holdableFormats: selectedFormats
349
350         }).toPromise().then(
351             request => {
352                 console.log('hold returned: ', request);
353                 ctx.lastRequest = request;
354                 ctx.processing = false;
355
356                 // If this request failed and was not already an override,
357                 // see of this user has permission to override.
358                 if (!request.override &&
359                     !request.result.success && request.result.evt) {
360
361                     const txtcode = request.result.evt.textcode;
362                     const perm = txtcode + '.override';
363
364                     return this.perm.hasWorkPermHere(perm).then(
365                         permResult => ctx.canOverride = permResult[perm]);
366                 }
367             },
368             error => {
369                 ctx.processing = false;
370                 console.error(error);
371             }
372         );
373     }
374
375     override(ctx: HoldContext) {
376         this.placeOneHold(ctx, true);
377     }
378
379     canOverride(ctx: HoldContext): boolean {
380         return ctx.lastRequest &&
381                 !ctx.lastRequest.result.success && ctx.canOverride;
382     }
383
384     iconFormatLabel(code: string): string {
385         return this.cat.iconFormatLabel(code);
386     }
387
388     // TODO: for now, only show meta filters for meta holds.
389     // Add an "advanced holds" option to display these for T hold.
390     hasMetaFilters(ctx: HoldContext): boolean {
391         return (
392             this.holdType === 'M' && // TODO
393             ctx.holdMeta.metarecord_filters && (
394                 ctx.holdMeta.metarecord_filters.langs.length > 1 ||
395                 ctx.holdMeta.metarecord_filters.formats.length > 1
396             )
397         );
398     }
399 }
400
401