LP1889128 Staffcat holds recipient / multi-hold repairs
authorBill Erickson <berickxx@gmail.com>
Tue, 25 Aug 2020 20:02:38 +0000 (16:02 -0400)
committerJane Sandberg <sandbej@linnbenton.edu>
Wed, 6 Jan 2021 00:48:27 +0000 (16:48 -0800)
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 <berickxx@gmail.com>
Signed-off-by: Michele Morgan <mmorgan@noblenet.org>
Signed-off-by: Jane Sandberg <sandbej@linnbenton.edu>
Open-ILS/src/eg2/src/app/staff/catalog/catalog.component.ts
Open-ILS/src/eg2/src/app/staff/catalog/catalog.service.ts
Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html
Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.ts
Open-ILS/src/eg2/src/app/staff/share/patron/patron.service.ts

index 5ce3712..340a28d 100644 (file)
@@ -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();
     }
 }
 
index a065274..63c5c4e 100644 (file)
@@ -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<void> = new EventEmitter<void>();
+
     // 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);
index a708708..f19055e 100644 (file)
@@ -4,11 +4,18 @@
 
 <div class="row">
   <div class="col-lg-4">
-    <h3 i18n>Place Hold 
-      <small *ngIf="user">
-       ({{user.family_name()}}, {{user.first_given_name()}})
-      </small>
-    </h3>
+    <ng-container *ngIf="badBarcode">
+      <div class="alert alert-danger" i18n>
+        Barcode '{{badBarcode}}' not found.
+      </div>
+    </ng-container>
+    <ng-container *ngIf="!badBarcode">
+      <h3 i18n>Place Hold
+        <small *ngIf="user">
+        ({{user.family_name()}}, {{user.first_given_name()}})
+        </small>
+      </h3>
+    </ng-container>
   </div>
   <div class="col-lg-2 text-right">
     <button class="btn btn-outline-dark btn-sm" (click)="searchPatrons()">
@@ -81,7 +88,7 @@
           </eg-date-select>
         </div>
       </div>
-      <div class="row mt-2">
+      <div class="row mt-2" *ngIf="multiHoldsActive">
         <div class="col-lg-6">
           <label for='multi-hold-count' i18n>Number of copies:</label>
         </div>
             <button class="btn btn-success" (click)="placeHolds()" 
               [disabled]="!user || placeHoldsClicked" i18n>Place Hold(s)</button>
 
-            <button class="btn btn-outline-dark ml-2" (click)="reset()" i18n>Reset</button>
+            <button class="btn btn-outline-dark ml-2" (click)="resetForm()" i18n>Reset</button>
           </li>
         </ul><!-- col -->
       </div><!-- row -->
index 74bf9f1..f798254 100644 (file)
@@ -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<any> {
 
-        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();
             }
         );
     }
index 9678c4d..32aa678 100644 (file)
@@ -29,6 +29,7 @@ export class PatronService {
     getByBarcode(barcode: string, pcrudOps?: any): Promise<IdlObject> {
         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?