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