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