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