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: [
PatronModule,
MarcEditModule,
HttpClientModule
+ BarcodesModule
],
providers: [
StaffCatalogService
-<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
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;
patronSearch: PatronSearchDialogComponent;
@ViewChild('smsCbox', {static: false}) smsCbox: ComboboxComponent;
+ @ViewChild('barcodeSelect') private barcodeSelect: BarcodeSelectComponent;
@ViewChild('activeDateAlert') private activeDateAlert: AlertDialogComponent;
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;
+<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>
-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',
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
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;
+ }
+ });
}
}
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';
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';
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;
}
});
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: '',
component: PatronComponent
}, {
path: 'bcsearch',
- component: PatronComponent
+ component: BcSearchComponent
+ }, {
+ path: 'bcsearch/:barcode',
+ component: BcSearchComponent
}, {
path: ':id/:tab',
component: PatronComponent,
</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>
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,
}
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',
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();
}
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: [
],
imports: [
StaffCommonModule,
- GridModule
+ GridModule,
+ BarcodesModule
],
exports: [
PatronSearchComponent,
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()
private auth: AuthService
) {}
- // TODO import barcodes.module instead
bcSearch(barcode: string): Observable<any> {
return this.net.request(
'open-ils.actor',
'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> {
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++) {