LP1889128 Staffcat holds recipient / multi-hold repairs user/berick/lp1889128-staffcat-multi-holds-v2
authorBill Erickson <berickxx@gmail.com>
Tue, 25 Aug 2020 20:02:38 +0000 (16:02 -0400)
committerBill Erickson <berickxx@gmail.com>
Tue, 25 Aug 2020 20:10:23 +0000 (16:10 -0400)
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>
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 95912a4..5674652 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';
@@ -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<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.
@@ -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);
index 2ee4e88..e720c2f 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 0daf064..411d98c 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';
@@ -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<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(_ => {
+
+            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();
             }
         );
     }
index e50fbb2..e658a79 100644 (file)
@@ -27,6 +27,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?