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';
20 holdMeta: HoldRequestTarget;
22 lastRequest: HoldRequest;
23 canOverride?: boolean;
27 constructor(target: number) {
28 this.holdTarget = target;
29 this.processing = false;
30 this.selectedFormats = {
31 // code => selected-boolean
39 templateUrl: 'hold.component.html'
41 export class HoldComponent implements OnInit {
44 holdTargets: number[];
59 holdContexts: HoldContext[];
60 recordSummaries: BibRecordSummary[];
62 currentUserBarcode: string;
63 smsCarriers: ComboboxEntry[];
66 placeHoldsClicked: boolean;
68 @ViewChild('patronSearch', {static: false})
69 patronSearch: PatronSearchDialogComponent;
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
86 this.holdContexts = [];
87 this.smsCarriers = [];
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';
96 if (!Array.isArray(this.holdTargets)) {
97 this.holdTargets = [this.holdTargets];
100 this.holdTargets = this.holdTargets.map(t => Number(t));
102 this.requestor = this.auth.user();
103 this.pickupLib = this.auth.user().ws_ou();
105 this.holdContexts = this.holdTargets.map(target => {
106 const ctx = new HoldContext(target);
110 if (this.holdFor === 'staff') {
111 this.holdForChanged();
114 this.getTargetMeta();
116 this.org.settings('sms.enable').then(sets => {
117 this.smsEnabled = sets['sms.enable'];
118 if (!this.smsEnabled) { return; }
120 this.pcrud.search('csc', {active: 't'}, {order_by: {csc: 'name'}})
121 .subscribe(carrier => {
122 this.smsCarriers.push({
124 label: carrier.name()
129 setTimeout(() => // Focus barcode input
130 this.renderer.selectRootElement('#patron-barcode').focus());
133 // Load the bib, call number, copy, etc. data associated with each target.
135 this.holds.getHoldTargetMeta(this.holdType, this.holdTargets)
137 this.holdContexts.filter(ctx => ctx.holdTarget === meta.target)
140 this.mrFiltersToSelectors(ctx);
145 // By default, all metarecord filters options are enabled.
146 mrFiltersToSelectors(ctx: HoldContext) {
147 if (this.holdType !== 'M') { return; }
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);
155 if (meta.metarecord_filters.langs) {
156 meta.metarecord_filters.langs.forEach(
157 ccvm => ctx.selectedFormats.langs[ccvm.code()] = true);
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} {
169 const meta = ctx.holdMeta;
170 const slf = ctx.selectedFormats;
171 const result: any = {};
173 const formats = Object.keys(slf.formats)
174 .filter(code => Boolean(slf.formats[code])); // user-selected
176 const langs = Object.keys(slf.langs)
177 .filter(code => Boolean(slf.langs[code])); // user-selected
179 const compiled: any = {};
181 if (formats.length > 0) {
183 formats.forEach(code => {
184 const ccvm = meta.metarecord_filters.formats.filter(
185 format => format.code() === code)[0];
193 if (langs.length > 0) {
195 langs.forEach(code => {
196 const ccvm = meta.metarecord_filters.langs.filter(
197 format => format.code() === code)[0];
205 if (Object.keys(compiled).length > 0) {
207 res[ctx.holdTarget] = JSON.stringify(compiled);
217 if (this.holdFor === 'patron') {
218 if (this.userBarcode) {
219 this.userBarcodeChanged();
222 // To bypass the dupe check.
223 this.currentUserBarcode = '_' + this.requestor.id();
224 this.getUser(this.requestor.id());
228 activeDateSelected(dateStr: string) {
229 this.activeDate = dateStr;
232 userBarcodeChanged() {
234 // Avoid simultaneous or duplicate lookups
235 if (this.userBarcode === this.currentUserBarcode) {
241 if (!this.userBarcode) {
247 this.currentUserBarcode = this.userBarcode;
251 'open-ils.actor.get_barcodes',
252 this.auth.token(), this.auth.user().ws_ou(),
253 'actor', this.userBarcode
254 ).subscribe(barcodes => {
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)) {
271 this.notifyEmail = true;
272 this.notifyPhone = true;
273 this.phoneValue = '';
274 this.pickupLib = this.requestor.ws_ou();
277 getUser(id: number) {
278 this.pcrud.retrieve('au', id, {flesh: 1, flesh_fields: {au: ['settings']}})
281 this.applyUserSettings();
285 applyUserSettings() {
286 if (!this.user || !this.user.settings()) { return; }
288 // Start with defaults.
289 this.phoneValue = this.user.day_phone() || this.user.evening_phone();
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();
296 this.user.settings().forEach(setting => {
297 const name = setting.name();
298 const value = setting.value();
300 if (value === '' || value === null) { return; }
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/));
309 case 'opac.default_pickup_location':
310 this.pickupLib = value;
315 if (!this.user.email()) {
316 this.notifyEmail = false;
319 if (!this.phoneValue) {
320 this.notifyPhone = false;
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;
330 const target = this.holdTargets[idx];
331 const ctx = this.holdContexts.filter(
332 c => c.holdTarget === target)[0];
334 this.placeOneHold(ctx).then(() => this.placeHolds(idx + 1));
337 placeOneHold(ctx: HoldContext, override?: boolean): Promise<any> {
339 ctx.processing = true;
340 const selectedFormats = this.mrSelectorsToFilters(ctx);
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,
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
359 console.log('hold returned: ', request);
360 ctx.lastRequest = request;
361 ctx.processing = false;
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) {
368 const txtcode = request.result.evt.textcode;
369 const perm = txtcode + '.override';
371 return this.perm.hasWorkPermHere(perm).then(
372 permResult => ctx.canOverride = permResult[perm]);
376 ctx.processing = false;
377 console.error(error);
382 override(ctx: HoldContext) {
383 this.placeOneHold(ctx, true);
386 canOverride(ctx: HoldContext): boolean {
387 return ctx.lastRequest &&
388 !ctx.lastRequest.result.success && ctx.canOverride;
391 iconFormatLabel(code: string): string {
392 return this.cat.iconFormatLabel(code);
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 {
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
408 this.patronSearch.open({size: 'xl'}).toPromise().then(
410 if (!patrons || patrons.length === 0) { return; }
412 const user = patrons[0];
416 this.currentUserBarcode = user.card().barcode();
417 user.home_ou(this.org.get(user.home_ou()).id()); // de-flesh
418 this.applyUserSettings();