From: Bill Erickson Date: Tue, 25 Aug 2020 20:02:38 +0000 (-0400) Subject: LP1889128 Staffcat holds recipient / multi-hold repairs X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=f12eea244fa9547dbf36fb865e030af3905ad0a7;p=working%2FEvergreen.git 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 --- 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 95912a4285..5674652d0d 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'; @@ -33,6 +33,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. @@ -59,7 +64,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 @@ -67,6 +75,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 2ee4e8825f..e720c2fc20 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 0daf064591..411d98ceef 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'; @@ -70,9 +71,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; @ViewChild('patronSearch', {static: false}) patronSearch: PatronSearchDialogComponent; @@ -80,7 +88,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, @@ -97,19 +104,12 @@ export class HoldComponent implements OnInit { this.smsCarriers = []; } - reset() { + ngOnInit() { - 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'; - if (this.staffCat.holdForBarcode) { - this.holdFor = 'patron'; - this.userBarcode = this.staffCat.holdForBarcode; - } - if (!Array.isArray(this.holdTargets)) { this.holdTargets = [this.holdTargets]; } @@ -119,47 +119,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; + } + + if (this.holdFor === 'staff' || this.userBarcode) { + this.holdForChanged(); + } + }); + + setTimeout(() => { + const node = document.getElementById('patron-barcode'); + if (node) { node.focus(); } + }); } - ngOnInit() { + 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(_ => { + + if (this.maxMultiHolds) { - setTimeout(() => // Focus barcode input - this.renderer.selectRootElement('#patron-barcode').focus()); + // 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[] { @@ -266,20 +282,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(); } @@ -290,17 +308,38 @@ export class HoldComponent implements OnInit { const promise = id ? this.patron.getById(id, flesh) : this.patron.getByBarcode(this.userBarcode); + 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() { @@ -401,7 +440,7 @@ export class HoldComponent implements OnInit { }).toPromise().then( request => { - console.log('hold returned: ', request); + console.debug('hold returned: ', request); ctx.lastRequest = request; ctx.processing = false; @@ -453,14 +492,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 e50fbb2422..e658a7962a 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 @@ -27,6 +27,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?