LP1904036 Barcode completion continued
authorBill Erickson <berickxx@gmail.com>
Thu, 18 Feb 2021 16:18:40 +0000 (11:18 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:23 +0000 (20:13 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.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/circ/patron/bcsearch.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts
Open-ILS/src/eg2/src/app/staff/share/barcodes/barcode-select.component.html
Open-ILS/src/eg2/src/app/staff/share/barcodes/barcode-select.component.ts
Open-ILS/src/eg2/src/app/staff/share/patron/patron.module.ts
Open-ILS/src/eg2/src/app/staff/share/patron/patron.service.ts

index 3222d87..58781da 100644 (file)
@@ -35,6 +35,7 @@ import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
 import {PreferencesComponent} from './prefs.component';
 import {BrowsePagerComponent} from './result/browse-pager.component';
 import {HttpClientModule} from '@angular/common/http';
+import {BarcodesModule} from '@eg/staff/share/barcodes/barcodes.module';
 
 @NgModule({
   declarations: [
@@ -75,6 +76,7 @@ import {HttpClientModule} from '@angular/common/http';
     PatronModule,
     MarcEditModule,
     HttpClientModule
+    BarcodesModule
   ],
   providers: [
     StaffCatalogService
index a935efc..c04c970 100644 (file)
@@ -1,6 +1,6 @@
 
-<eg-patron-search-dialog #patronSearch>
-</eg-patron-search-dialog>
+<eg-patron-search-dialog #patronSearch></eg-patron-search-dialog>
+<eg-barcode-select #barcodeSelect></eg-barcode-select>
 
 <eg-alert-dialog #activeDateAlert
   i18n-dialogTitle i18n-dialogBody
index e9c6893..f7c138e 100644 (file)
@@ -19,6 +19,9 @@ import {PatronService} from '@eg/staff/share/patron/patron.service';
 import {PatronSearchDialogComponent
   } from '@eg/staff/share/patron/search-dialog.component';
 import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+    } from '@eg/staff/share/patron/search-dialog.component';
+import {BarcodeSelectComponent
+    } from '@eg/staff/share/barcodes/barcode-select.component';
 
 class HoldContext {
     holdMeta: HoldRequestTarget;
@@ -98,6 +101,7 @@ export class HoldComponent implements OnInit {
       patronSearch: PatronSearchDialogComponent;
 
     @ViewChild('smsCbox', {static: false}) smsCbox: ComboboxComponent;
+    @ViewChild('barcodeSelect') private barcodeSelect: BarcodeSelectComponent;
 
     @ViewChild('activeDateAlert') private activeDateAlert: AlertDialogComponent;
 
@@ -393,9 +397,18 @@ export class HoldComponent implements OnInit {
         const flesh = {flesh: 1, flesh_fields: {au: ['settings']}};
 
         promise = promise.then(_ => {
-            return id ?
-                this.patron.getById(id, flesh) :
-                this.patron.getByBarcode(this.userBarcode, flesh);
+            if (id) { return id; }
+            // Find the patron ID from the provided barcode.
+            return this.barcodeSelect.getBarcode('actor', this.userBarcode)
+                .then(selection => selection ? selection.id : null);
+        });
+
+        promise = promise.then(matchId => {
+            if (matchId) {
+                return this.patron.getById(matchId, flesh);
+            } else {
+                return null;
+            }
         });
 
         this.badBarcode = null;
index db013a6..8f97db1 100644 (file)
@@ -1,14 +1,25 @@
+<eg-staff-banner i18n-bannerText bannerText="Find Patron By Barcode">
+</eg-staff-banner>
+<eg-barcode-select #barcodeSelect></eg-barcode-select>
 
-<div class="col-lg-4">
-  <div class="input-group">
-    <div class="input-group-prepend">
-      <span class="input-group-text" i18n>Barcode:</span>
+<div class="row">
+  <div class="col-lg-4">
+    <div class="input-group">
+      <div class="input-group-prepend">
+        <span class="input-group-text" i18n>Patron Barcode:</span>
+      </div>
+      <input type='text' id='barcode-search-input' class="form-control" 
+        placeholder="Barcode" i18n-placeholder [(ngModel)]='barcode'
+        (keydown.enter)="findUser()"/>
+      <div class="input-group-append">
+        <button class="btn btn-outline-secondary" 
+          (click)="findUser()" i18n>Submit</button>
+      </div>
     </div>
-    <input type='text' id='barcode-search-input' class="form-control" 
-      placeholder="Barcode" i18n-placeholder [ngModel]='barcode'/>
-    <div class="input-group-append">
-      <button class="btn btn-outline-secondary" 
-        (click)="findUser()" i18n>Submit</button>
+  </div>
+  <div class="col-lg-4">
+    <div *ngIf="notFound" class="alert alert-warning" i18n>
+      No match found for barcode "{{barcode}}".
     </div>
   </div>
 </div>
index ddd0433..59a5082 100644 (file)
@@ -1,7 +1,8 @@
-import {Component, OnInit, AfterViewInit} from '@angular/core';
-import {ActivatedRoute} from '@angular/router';
+import {Component, OnInit, AfterViewInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute} from '@angular/router';
 import {NetService} from '@eg/core/net.service';
 import {AuthService} from '@eg/core/auth.service';
+import {BarcodeSelectComponent} from '@eg/staff/share/barcodes/barcode-select.component';
 
 @Component({
   templateUrl: 'bcsearch.component.html',
@@ -10,9 +11,12 @@ import {AuthService} from '@eg/core/auth.service';
 
 export class BcSearchComponent implements OnInit, AfterViewInit {
 
+    notFound = false;
     barcode = '';
+    @ViewChild('barcodeSelect') private barcodeSelect: BarcodeSelectComponent;
 
     constructor(
+        private router: Router,
         private route: ActivatedRoute,
         private net: NetService,
         private auth: AuthService
@@ -20,17 +24,24 @@ export class BcSearchComponent implements OnInit, AfterViewInit {
 
     ngOnInit() {
         this.barcode = this.route.snapshot.paramMap.get('barcode');
-        if (this.barcode) {
-            this.findUser();
-        }
     }
 
     ngAfterViewInit() {
-        document.getElementById('barcode-search-input').focus();
+        const node = document.getElementById('barcode-search-input');
+        if (node) { node.focus(); }
+        if (this.barcode) { this.findUser(); }
     }
 
     findUser(): void {
-        alert('Searching for user ' + this.barcode);
+        this.notFound = false;
+        this.barcodeSelect.getBarcode('actor', this.barcode)
+        .then(selection => {
+            if (selection && selection.id) {
+                this.router.navigate(['/staff/circ/patron', selection.id, 'checkout']);
+            } else {
+                this.notFound = true;
+            }
+        });
     }
 }
 
index 27baba5..f7e5b4a 100644 (file)
@@ -8,7 +8,8 @@ import {OrgService} from '@eg/core/org.service';
 import {NetService} from '@eg/core/net.service';
 import {PatronService} from '@eg/staff/share/patron/patron.service';
 import {PatronManagerService, CircGridEntry} from './patron.service';
-import {CheckoutParams, CheckoutResult, CircService} from '@eg/staff/share/circ/circ.service';
+import {CheckoutParams, CheckoutResult, CircService
+    } from '@eg/staff/share/circ/circ.service';
 import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
 import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
 import {GridComponent} from '@eg/share/grid/grid.component';
@@ -17,8 +18,10 @@ import {StoreService} from '@eg/core/store.service';
 import {ServerStoreService} from '@eg/core/server-store.service';
 import {PrecatCheckoutDialogComponent} from './precat-dialog.component';
 import {AudioService} from '@eg/share/util/audio.service';
-import {CopyAlertsDialogComponent} from '@eg/staff/share/holdings/copy-alerts-dialog.component';
-import {BarcodeSelectComponent} from '@eg/staff/share/barcodes/barcode-select.component';
+import {CopyAlertsDialogComponent
+    } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {BarcodeSelectComponent
+    } from '@eg/staff/share/barcodes/barcode-select.component';
 
 const SESSION_DUE_DATE = 'eg.circ.checkout.is_until_logout';
 
@@ -107,11 +110,12 @@ export class CheckoutComponent implements OnInit {
             if (this.dueDateOptions > 0) { params.due_date = this.dueDate; }
 
             return this.barcodeSelect.getBarcode('asset', this.checkoutBarcode)
-            .then(barcode => {
-                if (barcode) {
-                    params.copy_barcode = barcode;
+            .then(selection => {
+                if (selection) {
+                    params.copy_barcode = selection.barcode;
                     return params;
                 } else {
+                    // User canceled the multi-match selection dialog.
                     return null;
                 }
             });
index 999d5f7..de04742 100644 (file)
@@ -1,6 +1,7 @@
 import {NgModule} from '@angular/core';
 import {RouterModule, Routes} from '@angular/router';
 import {PatronComponent} from './patron.component';
+import {BcSearchComponent} from './bcsearch.component';
 
 const routes: Routes = [{
     path: '',
@@ -15,7 +16,10 @@ const routes: Routes = [{
     component: PatronComponent
   }, {
     path: 'bcsearch',
-    component: PatronComponent
+    component: BcSearchComponent
+  }, {
+    path: 'bcsearch/:barcode',
+    component: BcSearchComponent
   }, {
     path: ':id/:tab',
     component: PatronComponent,
index 9081ce8..4397674 100644 (file)
     </button>
   </div>
   <div class="modal-body">
-         <ng-container *ngFor="let barcode of barcodes">
-                 <div class="form-check">
+    <div class="alert alert-primary m-1 mb-3" i18n>Select the desired barcode.</div>
+         <ng-container *ngFor="let match of matches">
+                 <div class="form-check mb-2">
                          <input class="form-check-input" type="checkbox" value="" 
-          id="barcode-check-{{barcode}}" [(ngModel)]="inputs[barcode]"
+          id="barcode-check-{{match.id}}" [(ngModel)]="inputs[match.id]"
           (ngModelChange)="selectionChanged()">
-                         <label class="form-check-label" for="barcode-check-{{barcode}}">
-          {{barcode}}
+                         <label class="form-check-label" for="barcode-check-{{match.id}}">
+          {{match.barcode}}
                          </label>
                  </div>
     </ng-container>
   </div>
   <div class="modal-footer">
-    <button type="button" class="btn btn-success" [disabled]="!selectedBarcode"
-      (click)="close(selectedBarcode)" i18n>Apply</button>
+    <button type="button" class="btn btn-success" [disabled]="!selected"
+      (click)="close(selected)" i18n>Select</button>
     <button type="button" class="btn btn-warning" 
       (click)="close()" i18n>Cancel</button>
   </div>
index 5374410..4191c1b 100644 (file)
@@ -10,19 +10,38 @@ import {EventService, EgEvent} from '@eg/core/event.service';
 import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
 import {DialogComponent} from '@eg/share/dialog/dialog.component';
 
-/* Suppor barcode completion for asset/actor/serial/booking data */
+/* Support barcode completion for barcoded asset/actor data.
+ *
+ * When multiple barcodes match, the user is presented with a selection
+ * dialog to chose the desired barcode.
+ *
+ * <eg-barcode-select #barcodeSelect></eg-barcode-select>
+ *
+ * @ViewChild('barcodeSelect') private barcodeSelect: BarcodeSelectComponent;
+ *
+ * this.barcodeSelect.getBarcode(value)
+ *   .then(barcode => console.log('found barcode', barcode));
+ */
+
+export interface BarcodeSelectResult {
+
+    // Will be the originally requested barcode when no match is found.
+    barcode: string;
+
+    // Will be null when no match is found.
+    id: number;
+}
 
 @Component({
   selector: 'eg-barcode-select',
   templateUrl: './barcode-select.component.html',
 })
 
-export class BarcodeSelectComponent
-    extends DialogComponent implements OnInit {
+export class BarcodeSelectComponent extends DialogComponent implements OnInit {
 
-    selectedBarcode: string;
-    barcodes: string[];
-    inputs: {[barcode: string]: boolean};
+    matches: BarcodeSelectResult[];
+    selected: BarcodeSelectResult;
+    inputs: {[id: number]: boolean};
 
     constructor(
         private modal: NgbModal,
@@ -37,18 +56,32 @@ export class BarcodeSelectComponent
     }
 
     selectionChanged() {
-        this.selectedBarcode = Object.keys(this.inputs)
-            .filter(barcode => this.inputs[barcode] === true)[0];
+        const id = Object.keys(this.inputs).map(id => Number(id))
+            .filter(id => this.inputs[id] === true)[0];
+
+        if (id) {
+            this.selected = this.matches.filter(match => match.id === id)[0];
+
+        } else {
+            this.selected = null;
+        }
     }
 
     // Returns promise of barcode
     // When multiple barcodes match, the user is asked to select one.
     // Returns promise of null if no match is found or the user cancels
     // the selection process.
-    getBarcode(class_: 'asset' | 'actor', barcode: string): Promise<string> {
-        this.barcodes = [];
+    getBarcode(class_: 'asset' | 'actor',
+        barcode: string): Promise<BarcodeSelectResult> {
+
+        this.matches = [];
         this.inputs = {};
 
+        const result: BarcodeSelectResult = {
+            barcode: barcode,
+            id: null
+        };
+
        let promise = this.net.request(
             'open-ils.actor',
             'open-ils.actor.get_barcodes',
@@ -58,18 +91,20 @@ export class BarcodeSelectComponent
 
         promise = promise.then(results => {
 
-            if (!results) { return null; }
+            if (!results) { return result; }
 
             results.forEach(result => {
                 if (!this.evt.parse(result)) {
-                    this.barcodes.push(result.barcode);
+                    this.matches.push(result);
                 }
             });
 
-            if (this.barcodes.length === 0) {
-                return null;
-            } else if (this.barcodes.length === 1) {
-                return this.barcodes[0];
+            if (this.matches.length === 0) {
+                return result;
+
+            } else if (this.matches.length === 1) {
+                return this.matches[0];
+
             } else {
                 return this.open().toPromise();
             }
index 9987c64..48ab175 100644 (file)
@@ -6,6 +6,7 @@ import {PatronSearchComponent} from './search.component';
 import {PatronSearchDialogComponent} from './search-dialog.component';
 import {ProfileSelectComponent} from './profile-select.component';
 import {PatronPenaltyDialogComponent} from './penalty-dialog.component';
+import {BarcodesModule} from '@eg/staff/share/barcodes/barcodes.module';
 
 @NgModule({
     declarations: [
@@ -16,7 +17,8 @@ import {PatronPenaltyDialogComponent} from './penalty-dialog.component';
     ],
     imports: [
         StaffCommonModule,
-        GridModule
+        GridModule,
+        BarcodesModule
     ],
     exports: [
         PatronSearchComponent,
index 9487c68..d0c98bf 100644 (file)
@@ -6,6 +6,7 @@ import {EventService} from '@eg/core/event.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {AuthService} from '@eg/core/auth.service';
 import {Observable} from 'rxjs';
+import {BarcodeSelectComponent} from '@eg/staff/share/barcodes/barcode-select.component';
 
 
 @Injectable()
@@ -18,7 +19,6 @@ export class PatronService {
         private auth: AuthService
     ) {}
 
-    // TODO import barcodes.module instead
     bcSearch(barcode: string): Observable<any> {
         return this.net.request(
             'open-ils.actor',
@@ -27,6 +27,10 @@ export class PatronService {
            'actor', barcode.trim());
     }
 
+    // XXX: This assumes the provided barcode only matches a single patron.
+    // Use the <eg-barcode-select> component instead when the provided
+    // barcode could match multiple patrons.
+    //
     // Note pcrudOps should be constructed from the perspective
     // of a user ('au') retrieval, not a barcode ('ac') retrieval.
     getByBarcode(barcode: string, pcrudOps?: any): Promise<IdlObject> {
@@ -35,7 +39,6 @@ export class PatronService {
             if (!barcodes) { return null; }
 
             // Use the first successful barcode response.
-            // TODO: What happens when there are multiple responses?
             // Use for-loop for early exit since we have async
             // action within the loop.
             for (let i = 0; i < barcodes.length; i++) {