From 39a2dee1f2eed4a443db8263aac900ce7ac7f640 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Tue, 25 Aug 2020 16:02:38 -0400 Subject: [PATCH] LP1889128 Staffcat holds recipient / multi-hold repairs 1. Modifying the patron barcode input either directly or via patron search now fully resets the form, including previously placed holds. 2. Modifying the hold receipient clears the previous "placing hold for patron" receipient applied from within the patron app, i.e. the banner along the top of the catalog page. 3. Hide the 'Number of copies' selector when multi-copy holds are not supported. 4. Hide the 'Number of copies' selector when the request does not have CREATE_DUPLICATE_HOLDS permissions for the currently selected 5. Display an error message when the barcode entered does not result in finding a patron. Signed-off-by: Bill Erickson Signed-off-by: Michele Morgan Signed-off-by: Jane Sandberg --- .../app/staff/catalog/catalog.component.ts | 8 +- .../src/app/staff/catalog/catalog.service.ts | 18 ++- .../staff/catalog/hold/hold.component.html | 21 ++- .../app/staff/catalog/hold/hold.component.ts | 135 +++++++++++------- .../app/staff/share/patron/patron.service.ts | 1 + 5 files changed, 124 insertions(+), 59 deletions(-) diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.ts index 5ce37128a0..340a28dba2 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.ts @@ -18,6 +18,11 @@ export class CatalogComponent implements OnInit { // child components. After initial creation, the context is // reset and updated as needed to apply new search parameters. this.staffCat.createContext(); + + // Subscribe to these emissions so that we can force + // change detection in this component even though the + // hold-for value was modified by a child component. + this.staffCat.holdForChange.subscribe(() => {}); } // Returns the 'au' object for the patron who we are @@ -27,8 +32,7 @@ export class CatalogComponent implements OnInit { } clearHoldPatron() { - this.staffCat.holdForUser = null; - this.staffCat.holdForBarcode = null; + this.staffCat.clearHoldPatron(); } } diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts index a0652744f6..63c5c4ed99 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@angular/core'; +import {Injectable, EventEmitter} from '@angular/core'; import {Router, ActivatedRoute} from '@angular/router'; import {IdlObject} from '@eg/core/idl.service'; import {OrgService} from '@eg/core/org.service'; @@ -36,6 +36,11 @@ export class StaffCatalogService { // User object for above barcode. holdForUser: IdlObject; + // Emit that the value has changed so components can detect + // the change even when the component is not itself digesting + // new values. + holdForChange: EventEmitter = new EventEmitter(); + // Cache the currently selected detail record (i.g. catalog/record/123) // summary so the record detail component can avoid duplicate fetches // during record tab navigation. @@ -65,7 +70,10 @@ export class StaffCatalogService { if (this.holdForBarcode) { this.patron.getByBarcode(this.holdForBarcode) - .then(user => this.holdForUser = user); + .then(user => { + this.holdForUser = user; + this.holdForChange.emit(); + }); } this.searchContext.org = this.org; // service, not searchOrg @@ -73,6 +81,12 @@ export class StaffCatalogService { this.applySearchDefaults(); } + clearHoldPatron() { + this.holdForUser = null; + this.holdForBarcode = null; + this.holdForChange.emit(); + } + cloneContext(context: CatalogSearchContext): CatalogSearchContext { const params: any = this.catUrl.toUrlParams(context); return this.catUrl.fromUrlHash(params); diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html index a708708895..f19055e0f6 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html @@ -4,11 +4,18 @@
-

Place Hold - - ({{user.family_name()}}, {{user.first_given_name()}}) - -

+ +
+ Barcode '{{badBarcode}}' not found. +
+
+ +

Place Hold + + ({{user.family_name()}}, {{user.first_given_name()}}) + +

+
-
+
@@ -170,7 +177,7 @@ - +
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.ts index 74bf9f189b..f7982549c4 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.ts @@ -1,5 +1,6 @@ -import {Component, OnInit, Input, ViewChild, Renderer2} from '@angular/core'; +import {Component, OnInit, Input, ViewChild} from '@angular/core'; import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {tap} from 'rxjs/operators'; import {EventService} from '@eg/core/event.service'; import {NetService} from '@eg/core/net.service'; import {AuthService} from '@eg/core/auth.service'; @@ -71,9 +72,16 @@ export class HoldComponent implements OnInit { smsCarriers: ComboboxEntry[]; smsEnabled: boolean; + maxMultiHolds = 0; + + // True if mult-copy holds are active for the current receipient. + multiHoldsActive = false; + + canPlaceMultiAt: number[] = []; multiHoldCount = 1; placeHoldsClicked: boolean; + badBarcode: string = null; puLibWsFallback = false; @@ -83,7 +91,6 @@ export class HoldComponent implements OnInit { constructor( private router: Router, private route: ActivatedRoute, - private renderer: Renderer2, private evt: EventService, private net: NetService, private org: OrgService, @@ -101,10 +108,15 @@ export class HoldComponent implements OnInit { this.smsCarriers = []; } - reset() { + ngOnInit() { + + // Respond to changes in hold type. This currently assumes hold + // types only toggle post-init between copy-level types (C,R,F) + // and no other params (e.g. target) change with it. If other + // types require tracking, additional data collection may be needed. + this.route.paramMap.subscribe( + (params: ParamMap) => this.holdType = params.get('type')); - this.user = null; - this.userBarcode = null; this.holdType = this.route.snapshot.params['type']; this.holdTargets = this.route.snapshot.queryParams['target']; this.holdFor = this.route.snapshot.queryParams['holdFor'] || 'patron'; @@ -126,54 +138,63 @@ export class HoldComponent implements OnInit { this.requestor = this.auth.user(); this.pickupLib = this.auth.user().ws_ou(); - this.holdContexts = this.holdTargets.map(target => { - const ctx = new HoldContext(target); - return ctx; - }); - this.resetForm(); - if (this.holdFor === 'staff' || this.userBarcode) { - this.holdForChanged(); - } + this.getRequestorSetsAndPerms() + .then(_ => { - this.getTargetMeta(); - this.placeHoldsClicked = false; - } + // Load receipient data if we have any. + if (this.staffCat.holdForBarcode) { + this.holdFor = 'patron'; + this.userBarcode = this.staffCat.holdForBarcode; + } - ngOnInit() { + if (this.holdFor === 'staff' || this.userBarcode) { + this.holdForChanged(); + } + }); - // Respond to changes in hold type. This currently assumes hold - // types only toggle post-init between copy-level types (C,R,F) - // and no other params (e.g. target) change with it. If other - // types require tracking, additional data collection may be needed. - this.route.paramMap.subscribe( - (params: ParamMap) => this.holdType = params.get('type')); + setTimeout(() => { + const node = document.getElementById('patron-barcode'); + if (node) { node.focus(); } + }); + } + + getRequestorSetsAndPerms(): Promise { - this.reset(); + return this.org.settings( + ['sms.enable', 'circ.holds.max_duplicate_holds']) - this.org.settings(['sms.enable', 'circ.holds.max_duplicate_holds']) .then(sets => { this.smsEnabled = sets['sms.enable']; + const max = Number(sets['circ.holds.max_duplicate_holds']); + if (Number(max) > 0) { this.maxMultiHolds = Number(max); } + if (this.smsEnabled) { - this.pcrud.search( + + return this.pcrud.search( 'csc', {active: 't'}, {order_by: {csc: 'name'}}) - .subscribe(carrier => { + .pipe(tap(carrier => { this.smsCarriers.push({ id: carrier.id(), label: carrier.name() }); - }); + })).toPromise(); } - const max = sets['circ.holds.max_duplicate_holds']; - if (Number(max) > 0) { this.maxMultiHolds = max; } - }); + }).then(_ => { - setTimeout(() => // Focus barcode input - this.renderer.selectRootElement('#patron-barcode').focus()); + if (this.maxMultiHolds) { + + // Multi-copy holds are supported. Let's see where this + // requestor has permission to take advantage of them. + return this.perm.hasWorkPermAt( + ['CREATE_DUPLICATE_HOLDS'], true).then(perms => + this.canPlaceMultiAt = perms['CREATE_DUPLICATE_HOLDS']); + } + }); } holdCountRange(): number[] { @@ -280,20 +301,22 @@ export class HoldComponent implements OnInit { } userBarcodeChanged() { + const newBc = this.userBarcode; + + if (!newBc) { this.user = null; return; } // Avoid simultaneous or duplicate lookups - if (this.userBarcode === this.currentUserBarcode) { - return; + if (newBc === this.currentUserBarcode) { return; } + + if (newBc !== this.staffCat.holdForBarcode) { + // If an alternate barcode is entered, it takes us out of + // place-hold-for-patron-x-from-search mode. + this.staffCat.clearHoldPatron(); } this.resetForm(); + this.userBarcode = newBc; // clobbered in reset - if (!this.userBarcode) { - this.user = null; - return; - } - - this.user = null; this.currentUserBarcode = this.userBarcode; this.getUser(); } @@ -304,17 +327,38 @@ export class HoldComponent implements OnInit { const promise = id ? this.patron.getById(id, flesh) : this.patron.getByBarcode(this.userBarcode, flesh); + this.badBarcode = null; promise.then(user => { + + if (!user) { + // IDs are assumed to valid + this.badBarcode = this.userBarcode; + return; + } + this.user = user; this.applyUserSettings(); + this.multiHoldsActive = + this.canPlaceMultiAt.includes(user.home_ou()); }); } resetForm() { + this.user = null; + this.userBarcode = null; this.notifyEmail = true; this.notifyPhone = true; this.phoneValue = ''; this.pickupLib = this.requestor.ws_ou(); + this.placeHoldsClicked = false; + + this.holdContexts = this.holdTargets.map(target => { + const ctx = new HoldContext(target); + return ctx; + }); + + // Required after rebuilding the contexts + this.getTargetMeta(); } applyUserSettings() { @@ -491,14 +535,9 @@ export class HoldComponent implements OnInit { this.patronSearch.open({size: 'xl'}).toPromise().then( patrons => { if (!patrons || patrons.length === 0) { return; } - const user = patrons[0]; - - this.user = user; - this.userBarcode = - this.currentUserBarcode = user.card().barcode(); - user.home_ou(this.org.get(user.home_ou()).id()); // de-flesh - this.applyUserSettings(); + this.userBarcode = user.card().barcode(); + this.userBarcodeChanged(); } ); } diff --git a/Open-ILS/src/eg2/src/app/staff/share/patron/patron.service.ts b/Open-ILS/src/eg2/src/app/staff/share/patron/patron.service.ts index 9678c4d8c1..32aa678e20 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/patron/patron.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/patron/patron.service.ts @@ -29,6 +29,7 @@ export class PatronService { getByBarcode(barcode: string, pcrudOps?: any): Promise { return this.bcSearch(barcode).toPromise() .then(barcodes => { + if (!barcodes) { return null; } // Use the first successful barcode response. // TODO: What happens when there are multiple responses? -- 2.43.2