From: Bill Erickson Date: Thu, 9 Jan 2020 22:20:42 +0000 (-0500) Subject: LP1859241 Angular holds patron search dialog X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=1e35be4827254524e229ead11f89c5de1340113d;p=evergreen%2Fjoelewis.git LP1859241 Angular holds patron search dialog Implements a patron search dialog which may be instantiated directly from the staff catalog holds placement interface. Includes: 1. New patron module (which absorbs the existing PatronService) 2. New patron search component 3. Patron search component dialog wrapper. 4. Patron profile selector component which understands custom group display trees. 4. Fixes an issue with the grid where the 'datatype' was not always propagated to IDL fields. 5. Modifies the combobox to allow the caller to clear the value by passing a null value for the selectedId. To Test: [1] Navigate to the Angular staff catalog [2] Perform a bib search [3] Click 'Place Hold' next to a title. [4] Click the 'Patron Search' button. [5] Search for patrons and either double-click a search result row or single click then chose the 'Select' button. [6] Confirm the selected patron is now chosen for holds placement. Signed-off-by: Bill Erickson Signed-off-by: Ruth Frasur --- diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts index 02839579d3..41d2b30cec 100644 --- a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts +++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts @@ -75,16 +75,19 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit { // Allow the selected entry ID to be passed via the template // This does NOT not emit onChange events. @Input() set selectedId(id: any) { - if (id) { - if (this.entrylist.length) { - this.selected = this.entrylist.filter(e => e.id === id)[0]; - } + if (id === undefined) { return; } - if (!this.selected) { - // It's possible the selected ID lives in a set of entries - // that are yet to be provided. - this.startId = id; - } + // clear on explicit null + if (id === null) { this.selected = null; } + + if (this.entrylist.length) { + this.selected = this.entrylist.filter(e => e.id === id)[0]; + } + + if (!this.selected) { + // It's possible the selected ID lives in a set of entries + // that are yet to be provided. + this.startId = id; } } diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts index e4f6715ef6..e885fb7787 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts @@ -230,9 +230,11 @@ export class GridColumnSet { if (idlInfo) { col.idlFieldDef = idlInfo.idlField; col.idlClass = idlInfo.idlClass.name; + if (!col.datatype) { + col.datatype = col.idlFieldDef.datatype; + } if (!col.label) { col.label = col.idlFieldDef.label || col.idlFieldDef.name; - col.datatype = col.idlFieldDef.datatype; } } } diff --git a/Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts b/Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts index 9b14137243..dbcfb03b8f 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/booking/booking.module.ts @@ -11,7 +11,7 @@ import {PickupComponent} from './pickup.component'; import {PullListComponent} from './pull-list.component'; import {ReturnComponent} from './return.component'; import {NoTimezoneSetComponent} from './no-timezone-set.component'; -import {PatronService} from '@eg/staff/share/patron.service'; +import {PatronModule} from '@eg/staff/share/patron/patron.module'; import {BookingResourceBarcodeValidatorDirective} from './booking_resource_validator.directive'; import {FmRecordEditorModule} from '@eg/share/fm-editor/fm-editor.module'; import {OrgFamilySelectModule} from '@eg/share/org-family-select/org-family-select.module'; @@ -23,9 +23,9 @@ import {OrgFamilySelectModule} from '@eg/share/org-family-select/org-family-sele BookingRoutingModule, ReactiveFormsModule, FmRecordEditorModule, - OrgFamilySelectModule + OrgFamilySelectModule, + PatronModule ], - providers: [PatronService], declarations: [ CancelReservationDialogComponent, CreateReservationComponent, diff --git a/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.ts index 028f7cf89f..076c4132ca 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/booking/pickup.component.ts @@ -2,7 +2,7 @@ import {Component, OnInit, ViewChild, OnDestroy} from '@angular/core'; import {Router, ActivatedRoute, ParamMap} from '@angular/router'; import {Subscription, of} from 'rxjs'; import {single, filter, switchMap, debounceTime, tap} from 'rxjs/operators'; -import {PatronService} from '@eg/staff/share/patron.service'; +import {PatronService} from '@eg/staff/share/patron/patron.service'; import {PcrudService} from '@eg/core/pcrud.service'; import {IdlObject} from '@eg/core/idl.service'; import {ReservationsGridComponent} from './reservations-grid.component'; diff --git a/Open-ILS/src/eg2/src/app/staff/booking/return.component.ts b/Open-ILS/src/eg2/src/app/staff/booking/return.component.ts index d7a42f491f..f37e10e8a3 100644 --- a/Open-ILS/src/eg2/src/app/staff/booking/return.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/booking/return.component.ts @@ -4,7 +4,7 @@ import {FormGroup, FormControl, Validators} from '@angular/forms'; import {NgbTabChangeEvent, NgbTabset} from '@ng-bootstrap/ng-bootstrap'; import {Observable, from, of, Subscription} from 'rxjs'; import { single, switchMap, tap, debounceTime } from 'rxjs/operators'; -import {PatronService} from '@eg/staff/share/patron.service'; +import {PatronService} from '@eg/staff/share/patron/patron.service'; import {PcrudService} from '@eg/core/pcrud.service'; import {IdlObject} from '@eg/core/idl.service'; import {ReservationsGridComponent} from './reservations-grid.component'; diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts index 3ad00a9942..9b7d57acdc 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts @@ -5,6 +5,7 @@ import {CatalogRoutingModule} from './routing.module'; import {HoldsModule} from '@eg/staff/share/holds/holds.module'; import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module'; import {BookingModule} from '@eg/staff/share/booking/booking.module'; +import {PatronModule} from '@eg/staff/share/patron/patron.module'; import {CatalogComponent} from './catalog.component'; import {SearchFormComponent} from './search-form.component'; import {ResultsComponent} from './result/results.component'; @@ -64,6 +65,7 @@ import {PreferencesComponent} from './prefs.component'; HoldsModule, HoldingsModule, BookingModule, + PatronModule, MarcEditModule ], providers: [ diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html index fa04d86ede..dca200dace 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html @@ -1,3 +1,7 @@ + + + +

Place Hold @@ -7,7 +11,7 @@

- +
+ + + + diff --git a/Open-ILS/src/eg2/src/app/staff/share/patron/search-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/patron/search-dialog.component.ts new file mode 100644 index 0000000000..98e1c22d72 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/patron/search-dialog.component.ts @@ -0,0 +1,36 @@ +import {Component, OnInit, Input, Output, ViewChild} from '@angular/core'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {DialogComponent} from '@eg/share/dialog/dialog.component'; +import {PatronSearchComponent} from './search.component'; + +/** + * Dialog container for patron search component + * + * + * + */ + +@Component({ + selector: 'eg-patron-search-dialog', + templateUrl: 'search-dialog.component.html' +}) + +export class PatronSearchDialogComponent + extends DialogComponent implements OnInit { + + @ViewChild('searchForm', {static: false}) + searchForm: PatronSearchComponent; + + constructor(private modal: NgbModal) { super(modal); } + + ngOnInit() {} + + // Fired when a row in the search grid is dbl-clicked / activated + patronsSelected(patrons: IdlObject[]) { + this.close(patrons); + } +} + + + diff --git a/Open-ILS/src/eg2/src/app/staff/share/patron/search.component.html b/Open-ILS/src/eg2/src/app/staff/share/patron/search.component.html new file mode 100644 index 0000000000..f2e363217e --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/patron/search.component.html @@ -0,0 +1,233 @@ + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+ + +
+
+ + + +
+
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ diff --git a/Open-ILS/src/eg2/src/app/staff/share/patron/search.component.ts b/Open-ILS/src/eg2/src/app/staff/share/patron/search.component.ts new file mode 100644 index 0000000000..43f4fe18db --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/patron/search.component.ts @@ -0,0 +1,239 @@ +import {Component, Input, Output, OnInit, AfterViewInit, + EventEmitter, ViewChild, Renderer2} from '@angular/core'; +import {Observable, of} from 'rxjs'; +import {map} from 'rxjs/operators'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {EventService} from '@eg/core/event.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {OrgService} from '@eg/core/org.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {ServerStoreService} from '@eg/core/server-store.service'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ComboboxEntry, ComboboxComponent} from '@eg/share/combobox/combobox.component'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource} from '@eg/share/grid/grid'; +import {Pager} from '@eg/share/util/pager'; + +const DEFAULT_SORT = [ + 'family_name ASC', + 'first_given_name ASC', + 'second_given_name ASC', + 'dob DESC' +]; + +const DEFAULT_FLESH = [ + 'card', 'settings', 'standing_penalties', 'addresses', 'billing_address', + 'mailing_address', 'stat_cat_entries', 'waiver_entries', 'usr_activity', + 'notes', 'profile' +]; + +const EXPAND_FORM = 'eg.circ.patron.search.show_extras'; +const INCLUDE_INACTIVE = 'eg.circ.patron.search.include_inactive'; + +@Component({ + selector: 'eg-patron-search', + templateUrl: './search.component.html' +}) + +export class PatronSearchComponent implements OnInit, AfterViewInit { + + @ViewChild('searchGrid', {static: false}) searchGrid: GridComponent; + + // Fired on dbl-click of a search result row. + @Output() patronsSelected: EventEmitter; + + search: any = {}; + searchOrg: IdlObject; + expandForm: boolean; + dataSource: GridDataSource; + profileGroups: IdlObject[] = []; + + constructor( + private renderer: Renderer2, + private net: NetService, + private org: OrgService, + private auth: AuthService, + private store: ServerStoreService + ) { + this.patronsSelected = new EventEmitter(); + this.dataSource = new GridDataSource(); + this.dataSource.getRows = (pager: Pager, sort: any[]) => { + return this.getRows(pager, sort); + }; + } + + ngOnInit() { + this.searchOrg = this.org.root(); + this.store.getItemBatch([EXPAND_FORM, INCLUDE_INACTIVE]) + .then(settings => { + this.expandForm = settings[EXPAND_FORM]; + this.search.inactive = settings[INCLUDE_INACTIVE]; + }); + } + + ngAfterViewInit() { + this.renderer.selectRootElement('#focus-this-input').focus(); + } + + toggleExpandForm() { + this.expandForm = !this.expandForm; + if (this.expandForm) { + this.store.setItem(EXPAND_FORM, true); + } else { + this.store.removeItem(EXPAND_FORM); + } + } + + toggleIncludeInactive() { + if (this.search.inactive) { // value set by ngModel + this.store.setItem(INCLUDE_INACTIVE, true); + } else { + this.store.removeItem(INCLUDE_INACTIVE); + } + } + + rowsSelected(rows: IdlObject | IdlObject[]) { + this.patronsSelected.emit([].concat(rows)); + } + + getSelected(): IdlObject[] { + return this.searchGrid ? + this.searchGrid.context.getSelectedRows() : []; + } + + go() { + this.searchGrid.reload(); + } + + clear() { + this.search = {profile: null}; + this.searchOrg = this.org.root(); + } + + getRows(pager: Pager, sort: any[]): Observable { + + let observable: Observable; + + if (this.search.id) { + observable = this.searchById(); + } else { + observable = this.searchByForm(pager, sort); + } + + return observable.pipe(map(user => this.localFleshUser(user))); + } + + localFleshUser(user: IdlObject): IdlObject { + user.home_ou(this.org.get(user.home_ou())); + return user; + } + + searchByForm(pager: Pager, sort: any[]): Observable { + + const search = this.compileSearch(); + if (!search) { return of(); } + + const sorter = this.compileSort(sort); + + return this.net.request( + 'open-ils.actor', + 'open-ils.actor.patron.search.advanced.fleshed', + this.auth.token(), + this.compileSearch(), + pager.limit, + sorter, + null, // ? + this.searchOrg.id(), + DEFAULT_FLESH, + pager.offset + ); + } + + searchById(): Observable { + return this.net.request( + 'open-ils.actor', + 'open-ils.actor.user.fleshed.retrieve', + this.auth.token(), this.search.id, DEFAULT_FLESH + ); + } + + compileSort(sort: any[]): string[] { + if (!sort || sort.length === 0) { return DEFAULT_SORT; } + return sort.map(def => `${def.name} ${def.dir}`); + } + + compileSearch(): any { + + let hasSearch = false; + const search: Object = {}; + + Object.keys(this.search).forEach(field => { + search[field] = this.mapSearchField(field); + if (search[field]) { hasSearch = true; } + }); + + return hasSearch ? search : null; + } + + isValue(val: any): boolean { + return (val !== null && val !== undefined && val !== ''); + } + + mapSearchField(field: string): any { + + const value = this.search[field]; + if (!this.isValue(value)) { return null; } + + const chunk = {value: value, group: 0}; + + switch (field) { + + case 'name': // name keywords + case 'inactive': + delete chunk.group; + break; + + case 'street1': + case 'street2': + case 'city': + case 'state': + case 'post_code': + chunk.group = 1; + break; + + case 'phone': + case 'ident': + chunk.group = 2; + break; + + case 'card': + chunk.group = 3; + break; + + case 'profile': + chunk.group = 5; + chunk.value = chunk.value.id(); // pgt object + break; + + case 'dob_day': + case 'dob_month': + case 'dob_year': + chunk.group = 4; + chunk.value = chunk.value.replace(/\D/g, ''); + + if (!field.match(/year/)) { + // force day/month to be 2 digits + chunk[field].value = ('0' + value).slice(-2); + } + break; + } + + // Confirm the value wasn't scrubbed away above + if (!this.isValue(chunk.value)) { return null; } + + return chunk; + } +} + diff --git a/Open-ILS/src/eg2/src/styles.css b/Open-ILS/src/eg2/src/styles.css index 169cf639b2..d1144fd9fb 100644 --- a/Open-ILS/src/eg2/src/styles.css +++ b/Open-ILS/src/eg2/src/styles.css @@ -89,6 +89,11 @@ h5 {font-size: .95rem} line-height: inherit; } +.mat-icon-shrunk-in-button { + line-height: inherit; + font-size: 18px; +} + .input-group .mat-icon-in-button { font-size: .88rem !important; /* useful for buttons that cuddle up with inputs */ }