LP#2009725 Place holds for recent patrons user/dbriem/lp2009725_place_holds_for_recent_patrons
authorDan Briem <dbriem@wlsmail.org>
Thu, 9 Mar 2023 20:17:45 +0000 (20:17 +0000)
committerDan Briem <dbriem@wlsmail.org>
Thu, 9 Mar 2023 20:17:45 +0000 (20:17 +0000)
Adds a button to the holds interface to retrieve the last patron
or show a dialog of recent patrons depending on the Number of
Retrievable Patrons org unit setting.

When a patron is loaded in the holds interface, they are added
to the recent patrons list.

- If the Number of Retrievable Recent Patron setting is < 1,
  the button doesn't appear.

- If the setting is 1 or unset (1 is default), clicking loads
  the last patron.

- If the setting is greater than 1, clicking shows a dialog of
  recent patrons to select from.

- If there are no recent patrons, the button is disabled.

Signed-off-by: Dan Briem <dbriem@wlsmail.org>
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/search-dialog.component.html
Open-ILS/src/eg2/src/app/staff/share/patron/search-dialog.component.ts

index d447492..844b2b8 100644 (file)
@@ -1,5 +1,8 @@
 
 <eg-patron-search-dialog #patronSearch></eg-patron-search-dialog>
+<eg-patron-search-dialog #recentPatronSearch
+  [patronIds]="recentPatronIds">
+</eg-patron-search-dialog>
 <eg-barcode-select #barcodeSelect></eg-barcode-select>
 <eg-worklog-strings-components></eg-worklog-strings-components>
 
       </h3>
     </ng-container>
   </div>
-  <div class="col-lg-2">
+  <div class="col-lg-6">
     <button class="btn btn-outline-dark btn-sm" (click)="searchPatrons()">
       <span class="material-icons mat-icon-in-button align-middle"
         i18n-title title="Search for Patron">search</span>
       <span class="align-middle" i18n>Search for Patron</span>
     </button>
+    <button class="btn btn-outline-dark btn-sm ml-2"
+      *ngIf="maxRecentPatrons > 0"
+      [disabled]="!recentPatrons().length"
+      (click)="searchRecentPatrons()">
+      <span class="material-icons mat-icon-in-button align-middle"
+        i18n-title title="Recent Patron(s)">person</span>
+      <span class="align-middle" i18n>Recent Patron(s)</span>
+    </button>
   </div>
 </div>
 
index a74bc37..22bbd69 100644 (file)
@@ -1,6 +1,6 @@
 import {Component, OnInit, ViewChild} from '@angular/core';
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
-import {tap} from 'rxjs/operators';
+import {filter, map} from 'rxjs/operators';
 import {EventService} from '@eg/core/event.service';
 import {NetService} from '@eg/core/net.service';
 import {AuthService} from '@eg/core/auth.service';
@@ -22,6 +22,7 @@ import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
 import {BarcodeSelectComponent
     } from '@eg/staff/share/barcodes/barcode-select.component';
 import {WorkLogService} from '@eg/staff/share/worklog/worklog.service';
+import {StoreService} from '@eg/core/store.service';
 
 class HoldContext {
     holdMeta: HoldRequestTarget;
@@ -97,8 +98,13 @@ export class HoldComponent implements OnInit {
     // Orgs which are not valid pickup locations
     disableOrgs: number[] = [];
 
+    maxRecentPatrons: number;
+    recentPatronIds: number[];
+
     @ViewChild('patronSearch', {static: false})
       patronSearch: PatronSearchDialogComponent;
+    @ViewChild('recentPatronSearch', {static: false})
+      recentPatronSearch: PatronSearchDialogComponent;
 
     @ViewChild('smsCbox', {static: false}) smsCbox: ComboboxComponent;
     @ViewChild('barcodeSelect') private barcodeSelect: BarcodeSelectComponent;
@@ -120,7 +126,8 @@ export class HoldComponent implements OnInit {
         private holds: HoldsService,
         private patron: PatronService,
         private perm: PermService,
-        private worklog: WorkLogService
+        private worklog: WorkLogService,
+        private localStore: StoreService
     ) {
         this.holdContexts = [];
         this.smsCarriers = [];
@@ -160,6 +167,9 @@ export class HoldComponent implements OnInit {
             }
         });
 
+        this.store.getItem('ui.staff.max_recent_patrons')
+        .then(setting => this.maxRecentPatrons = setting ?? 1);
+
         this.net.request('open-ils.actor',
             'open-ils.actor.settings.value_for_all_orgs',
             null, 'opac.holds.org_unit_not_pickup_lib'
@@ -425,6 +435,7 @@ export class HoldComponent implements OnInit {
             this.applyUserSettings();
             this.multiHoldsActive =
                 this.canPlaceMultiAt.includes(user.home_ou());
+            this.addRecentPatron(user.id());
         });
     }
 
@@ -722,6 +733,56 @@ export class HoldComponent implements OnInit {
         );
     }
 
+    searchRecentPatrons(): void {
+        const recentPatronIds: number[] = this.recentPatrons();
+
+        if (this.maxRecentPatrons < 1 || !recentPatronIds.length) {
+            return;
+        }
+
+        if (this.maxRecentPatrons > 1 && recentPatronIds.length > 1) {
+            // open dialog initialized with recent patrons
+            this.recentPatronIds = recentPatronIds.slice(
+                0, this.maxRecentPatrons
+            );
+            this.recentPatronSearch.open({size: 'xl'}).pipe(
+                filter(patrons => Boolean(patrons?.length)),
+                map(patrons => patrons[0])
+            ).subscribe(user => {
+                this.userBarcode = user.card().barcode();
+                this.userBarcodeChanged();
+            });
+        } else {
+            // use the most recent patron's barcode
+            this.patron.getFleshedById(recentPatronIds[0])
+            .then(user => {
+                if (user) {
+                    this.userBarcode = user.card().barcode();
+                    this.userBarcodeChanged();
+                }
+            });
+        }
+    }
+
+    recentPatrons(): number[] {
+        return this.localStore.getLoginSessionItem(
+            'eg.circ.recent_patrons'
+        ) || [];
+    }
+
+    addRecentPatron(id: number): void {
+        if (!id || this.maxRecentPatrons < 1) { return; }
+
+        // remove existing matches and make space for the ID
+        const recentPatrons: number[] = this.recentPatrons()
+        .filter(recentId => recentId !== id)
+        .slice(0, this.maxRecentPatrons - 1);
+
+        this.localStore.setLoginSessionItem(
+            'eg.circ.recent_patrons', [id, ...recentPatrons]
+        );
+    }
+
     isItemHold(): boolean {
         return this.holdType === 'C'
             || this.holdType === 'R'
index 1006a63..383b3ba 100644 (file)
@@ -7,7 +7,9 @@
     </button>
   </div>
   <div class="modal-body">
-    <eg-patron-search #searchForm (patronsSelected)="patronsSelected($event)">
+    <eg-patron-search #searchForm
+      [patronIds]="patronIds"
+      (patronsSelected)="patronsSelected($event)">
     </eg-patron-search>
   </div>
   <div class="modal-footer">
index 93c6119..a2fbdad 100644 (file)
@@ -1,4 +1,4 @@
-import {Component, ViewChild} from '@angular/core';
+import {Component, Input, ViewChild} from '@angular/core';
 import {IdlObject} from '@eg/core/idl.service';
 import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
 import {DialogComponent} from '@eg/share/dialog/dialog.component';
@@ -19,6 +19,9 @@ import {PatronSearchComponent} from './search.component';
 export class PatronSearchDialogComponent
     extends DialogComponent {
 
+    // If set, these patrons will be loaded automatically
+    @Input() patronIds: number[];
+
     @ViewChild('searchForm', {static: false})
         searchForm: PatronSearchComponent;