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 {ServerStoreService} from '@eg/core/server-store.service';
11 import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
12 import {CatalogService} from '@eg/share/catalog/catalog.service';
13 import {StaffCatalogService} from '../catalog.service';
14 import {HoldsService, HoldRequest,
15 HoldRequestTarget} from '@eg/staff/share/holds/holds.service';
16 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
17 import {PatronService} from '@eg/staff/share/patron/patron.service';
18 import {PatronSearchDialogComponent
19 } from '@eg/staff/share/patron/search-dialog.component';
22 holdMeta: HoldRequestTarget;
24 lastRequest: HoldRequest;
25 canOverride?: boolean;
29 constructor(target: number) {
30 this.holdTarget = target;
31 this.processing = false;
32 this.selectedFormats = {
33 // code => selected-boolean
41 templateUrl: 'hold.component.html'
43 export class HoldComponent implements OnInit {
46 holdTargets: number[];
61 holdContexts: HoldContext[];
62 recordSummaries: BibRecordSummary[];
64 currentUserBarcode: string;
65 smsCarriers: ComboboxEntry[];
68 placeHoldsClicked: boolean;
70 puLibWsFallback = false;
72 @ViewChild('patronSearch', {static: false})
73 patronSearch: PatronSearchDialogComponent;
76 private router: Router,
77 private route: ActivatedRoute,
78 private renderer: Renderer2,
79 private evt: EventService,
80 private net: NetService,
81 private org: OrgService,
82 private store: ServerStoreService,
83 private auth: AuthService,
84 private pcrud: PcrudService,
85 private bib: BibRecordService,
86 private cat: CatalogService,
87 private staffCat: StaffCatalogService,
88 private holds: HoldsService,
89 private patron: PatronService,
90 private perm: PermService
92 this.holdContexts = [];
93 this.smsCarriers = [];
98 // Respond to changes in hold type. This currently assumes hold
99 // types only toggle post-init between copy-level types (C,R,F)
100 // and no other params (e.g. target) change with it. If other
101 // types require tracking, additional data collection may be needed.
102 this.route.paramMap.subscribe(
103 (params: ParamMap) => this.holdType = params.get('type'));
105 this.holdType = this.route.snapshot.params['type'];
106 this.holdTargets = this.route.snapshot.queryParams['target'];
107 this.holdFor = this.route.snapshot.queryParams['holdFor'] || 'patron';
109 if (this.staffCat.holdForBarcode) {
110 this.holdFor = 'patron';
111 this.userBarcode = this.staffCat.holdForBarcode;
114 this.store.getItem('circ.staff_placed_holds_fallback_to_ws_ou')
115 .then(setting => this.puLibWsFallback = setting === true);
117 if (!Array.isArray(this.holdTargets)) {
118 this.holdTargets = [this.holdTargets];
121 this.holdTargets = this.holdTargets.map(t => Number(t));
123 this.requestor = this.auth.user();
124 this.pickupLib = this.auth.user().ws_ou();
126 this.holdContexts = this.holdTargets.map(target => {
127 const ctx = new HoldContext(target);
131 if (this.holdFor === 'staff' || this.userBarcode) {
132 this.holdForChanged();
135 this.getTargetMeta();
137 this.org.settings('sms.enable').then(sets => {
138 this.smsEnabled = sets['sms.enable'];
139 if (!this.smsEnabled) { return; }
141 this.pcrud.search('csc', {active: 't'}, {order_by: {csc: 'name'}})
142 .subscribe(carrier => {
143 this.smsCarriers.push({
145 label: carrier.name()
150 setTimeout(() => // Focus barcode input
151 this.renderer.selectRootElement('#patron-barcode').focus());
154 // Load the bib, call number, copy, etc. data associated with each target.
156 this.holds.getHoldTargetMeta(this.holdType, this.holdTargets)
158 this.holdContexts.filter(ctx => ctx.holdTarget === meta.target)
161 this.mrFiltersToSelectors(ctx);
166 // By default, all metarecord filters options are enabled.
167 mrFiltersToSelectors(ctx: HoldContext) {
168 if (this.holdType !== 'M') { return; }
170 const meta = ctx.holdMeta;
171 if (meta.metarecord_filters) {
172 if (meta.metarecord_filters.formats) {
173 meta.metarecord_filters.formats.forEach(
174 ccvm => ctx.selectedFormats.formats[ccvm.code()] = true);
176 if (meta.metarecord_filters.langs) {
177 meta.metarecord_filters.langs.forEach(
178 ccvm => ctx.selectedFormats.langs[ccvm.code()] = true);
183 // Map the selected metarecord filters optoins to a JSON-encoded
184 // list of attr filters as required by the API.
185 // Compiles a blob of
186 // {target: JSON({"0": [{_attr: ctype, _val: code}, ...], "1": [...]})}
187 // TODO: this should live in the hold service, not in the UI code.
188 mrSelectorsToFilters(ctx: HoldContext): {[target: number]: string} {
190 const meta = ctx.holdMeta;
191 const slf = ctx.selectedFormats;
192 const result: any = {};
194 const formats = Object.keys(slf.formats)
195 .filter(code => Boolean(slf.formats[code])); // user-selected
197 const langs = Object.keys(slf.langs)
198 .filter(code => Boolean(slf.langs[code])); // user-selected
200 const compiled: any = {};
202 if (formats.length > 0) {
204 formats.forEach(code => {
205 const ccvm = meta.metarecord_filters.formats.filter(
206 format => format.code() === code)[0];
214 if (langs.length > 0) {
216 langs.forEach(code => {
217 const ccvm = meta.metarecord_filters.langs.filter(
218 format => format.code() === code)[0];
226 if (Object.keys(compiled).length > 0) {
228 res[ctx.holdTarget] = JSON.stringify(compiled);
238 if (this.holdFor === 'patron') {
239 if (this.userBarcode) {
240 this.userBarcodeChanged();
243 // To bypass the dupe check.
244 this.currentUserBarcode = '_' + this.requestor.id();
245 this.getUser(this.requestor.id());
249 activeDateSelected(dateStr: string) {
250 this.activeDate = dateStr;
253 userBarcodeChanged() {
255 // Avoid simultaneous or duplicate lookups
256 if (this.userBarcode === this.currentUserBarcode) {
262 if (!this.userBarcode) {
268 this.currentUserBarcode = this.userBarcode;
272 getUser(id?: number) {
273 const flesh = {flesh: 1, flesh_fields: {au: ['settings']}};
275 const promise = id ? this.patron.getById(id, flesh) :
276 this.patron.getByBarcode(this.userBarcode, flesh);
278 promise.then(user => {
280 this.applyUserSettings();
285 this.notifyEmail = true;
286 this.notifyPhone = true;
287 this.phoneValue = '';
288 this.pickupLib = this.requestor.ws_ou();
291 applyUserSettings() {
292 if (!this.user) { return; }
294 // Start with defaults.
295 this.phoneValue = this.user.day_phone() || this.user.evening_phone();
297 // Default to work org if placing holds for staff.
298 if (this.user.id() !== this.requestor.id() && !this.puLibWsFallback) {
299 // This value may be superseded below by user settings.
300 this.pickupLib = this.user.home_ou();
303 if (!this.user.settings()) { return; }
305 this.user.settings().forEach(setting => {
306 const name = setting.name();
307 let value = setting.value();
309 if (value === '' || value === null) { return; }
311 // When fleshing 'settings' on the actor.usr object,
312 // we're grabbing the raw JSON values.
313 value = JSON.parse(value);
316 case 'opac.hold_notify':
317 this.notifyPhone = Boolean(value.match(/phone/));
318 this.notifyEmail = Boolean(value.match(/email/));
319 this.notifySms = Boolean(value.match(/sms/));
322 case 'opac.default_pickup_location':
323 this.pickupLib = Number(value);
328 if (!this.user.email()) {
329 this.notifyEmail = false;
332 if (!this.phoneValue) {
333 this.notifyPhone = false;
337 // Attempt hold placement on all targets
338 placeHolds(idx?: number) {
339 if (!idx) { idx = 0; }
340 if (!this.holdTargets[idx]) {
341 this.placeHoldsClicked = false;
344 this.placeHoldsClicked = true;
346 const target = this.holdTargets[idx];
347 const ctx = this.holdContexts.filter(
348 c => c.holdTarget === target)[0];
350 this.placeOneHold(ctx).then(() => this.placeHolds(idx + 1));
353 placeOneHold(ctx: HoldContext, override?: boolean): Promise<any> {
355 ctx.processing = true;
356 const selectedFormats = this.mrSelectorsToFilters(ctx);
358 let hType = this.holdType;
359 let hTarget = ctx.holdTarget;
360 if (hType === 'T' && ctx.holdMeta.part) {
361 // A Title hold morphs into a Part hold at hold placement time
362 // if a part is selected. This can happen on a per-hold basis
363 // when placing T-level holds.
365 hTarget = ctx.holdMeta.part.id();
368 console.debug(`Placing ${hType}-type hold on ${hTarget}`);
370 return this.holds.placeHold({
373 recipient: this.user.id(),
374 requestor: this.requestor.id(),
375 pickupLib: this.pickupLib,
377 notifyEmail: this.notifyEmail, // bool
378 notifyPhone: this.notifyPhone ? this.phoneValue : null,
379 notifySms: this.notifySms ? this.smsValue : null,
380 smsCarrier: this.notifySms ? this.smsCarrier : null,
381 thawDate: this.suspend ? this.activeDate : null,
382 frozen: this.suspend,
383 holdableFormats: selectedFormats
387 ctx.lastRequest = request;
388 ctx.processing = false;
390 if (!request.result.success) {
391 console.debug('hold failed with: ', request);
393 // If this request failed and was not already an override,
394 // see of this user has permission to override.
395 if (!request.override && request.result.evt) {
397 const txtcode = request.result.evt.textcode;
398 const perm = txtcode + '.override';
400 return this.perm.hasWorkPermHere(perm).then(
401 permResult => ctx.canOverride = permResult[perm]);
406 ctx.processing = false;
407 console.error(error);
412 override(ctx: HoldContext) {
413 this.placeOneHold(ctx, true);
416 canOverride(ctx: HoldContext): boolean {
417 return ctx.lastRequest &&
418 !ctx.lastRequest.result.success && ctx.canOverride;
421 iconFormatLabel(code: string): string {
422 return this.cat.iconFormatLabel(code);
425 // TODO: for now, only show meta filters for meta holds.
426 // Add an "advanced holds" option to display these for T hold.
427 hasMetaFilters(ctx: HoldContext): boolean {
429 this.holdType === 'M' && // TODO
430 ctx.holdMeta.metarecord_filters && (
431 ctx.holdMeta.metarecord_filters.langs.length > 1 ||
432 ctx.holdMeta.metarecord_filters.formats.length > 1
438 this.patronSearch.open({size: 'xl'}).toPromise().then(
440 if (!patrons || patrons.length === 0) { return; }
442 const user = patrons[0];
446 this.currentUserBarcode = user.card().barcode();
447 user.home_ou(this.org.get(user.home_ou()).id()); // de-flesh
448 this.applyUserSettings();
453 isItemHold(): boolean {
454 return this.holdType === 'C'
455 || this.holdType === 'R'
456 || this.holdType === 'F';
459 setPart(ctx: HoldContext, $event) {
460 const partId = $event.target.value;
463 ctx.holdMeta.parts.filter(p => +p.id() === +partId)[0];
465 ctx.holdMeta.part = null;