LP1904036 Angular Patron UI initial structures
authorBill Erickson <berickxx@gmail.com>
Wed, 12 Aug 2020 16:50:59 +0000 (12:50 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:22 +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>
29 files changed:
Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.html [deleted file]
Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.ts [deleted file]
Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.module.ts [deleted file]
Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/routing.module.ts [deleted file]
Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/edit-toolbar.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/edit-toolbar.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/summary.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/summary.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/summary.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts
Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.html
Open-ILS/src/eg2/src/app/staff/share/holds/grid.component.ts
Open-ILS/src/eg2/src/app/staff/share/patron/patron.service.ts
Open-ILS/src/eg2/src/app/staff/share/patron/search.component.html
Open-ILS/src/eg2/src/app/staff/share/patron/search.component.ts
Open-ILS/src/eg2/src/styles.css
Open-ILS/src/sql/Pg/upgrade/XXXX.data.angular-patron.sql [new file with mode: 0644]

diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch.component.html
new file mode 100644 (file)
index 0000000..db013a6
--- /dev/null
@@ -0,0 +1,16 @@
+
+<div class="col-lg-4">
+  <div class="input-group">
+    <div class="input-group-prepend">
+      <span class="input-group-text" i18n>Barcode:</span>
+    </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>
+</div>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch.component.ts
new file mode 100644 (file)
index 0000000..ddd0433
--- /dev/null
@@ -0,0 +1,37 @@
+import {Component, OnInit, AfterViewInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+
+@Component({
+  templateUrl: 'bcsearch.component.html',
+  selector: 'eg-patron-barcode-search'
+})
+
+export class BcSearchComponent implements OnInit, AfterViewInit {
+
+    barcode = '';
+
+    constructor(
+        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();
+    }
+
+    findUser(): void {
+        alert('Searching for user ' + this.barcode);
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.html
deleted file mode 100644 (file)
index e83cf9e..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-
-<eg-staff-banner bannerText="Search for Patron by Barcode" i18n-bannerText>
-</eg-staff-banner>
-
-<div class="col-lg-4">
-  <div class="input-group">
-    <div class="input-group-prepend">
-      <span class="input-group-text" i18n>Barcode:</span>
-    </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>
-</div>
-
-
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.component.ts
deleted file mode 100644 (file)
index dac5048..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-import {Component, OnInit, Renderer2} from '@angular/core';
-import {ActivatedRoute} from '@angular/router';
-import {NetService} from '@eg/core/net.service';
-import {AuthService} from '@eg/core/auth.service';
-
-@Component({
-  templateUrl: 'bcsearch.component.html'
-})
-
-export class BcSearchComponent implements OnInit {
-
-    barcode = '';
-
-    constructor(
-        private route: ActivatedRoute,
-        private renderer: Renderer2,
-        private net: NetService,
-        private auth: AuthService
-    ) {}
-
-    ngOnInit() {
-
-        this.renderer.selectRootElement('#barcode-search-input').focus();
-        this.barcode = this.route.snapshot.paramMap.get('barcode');
-
-        if (this.barcode) {
-            this.findUser();
-        }
-    }
-
-    findUser(): void {
-        alert('Searching for user ' + this.barcode);
-    }
-}
-
-
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/bcsearch.module.ts
deleted file mode 100644 (file)
index d1b16df..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-import {NgModule} from '@angular/core';
-import {StaffCommonModule} from '@eg/staff/common.module';
-import {BcSearchRoutingModule} from './routing.module';
-import {BcSearchComponent} from './bcsearch.component';
-
-@NgModule({
-  declarations: [
-    BcSearchComponent
-  ],
-  imports: [
-    StaffCommonModule,
-    BcSearchRoutingModule,
-  ],
-})
-
-export class BcSearchModule {}
-
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/bcsearch/routing.module.ts
deleted file mode 100644 (file)
index ce6783d..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-import {NgModule} from '@angular/core';
-import {RouterModule, Routes} from '@angular/router';
-import {BcSearchComponent} from './bcsearch.component';
-
-const routes: Routes = [
-  { path: '',
-    component: BcSearchComponent
-  },
-  { path: ':barcode',
-    component: BcSearchComponent
-  },
-];
-
-@NgModule({
-  imports: [RouterModule.forChild(routes)],
-  exports: [RouterModule]
-})
-
-export class BcSearchRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html
new file mode 100644 (file)
index 0000000..d6add2b
--- /dev/null
@@ -0,0 +1,2 @@
+
+CHECKOUT
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts
new file mode 100644 (file)
index 0000000..e300bbe
--- /dev/null
@@ -0,0 +1,25 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+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} from './patron.service';
+
+@Component({
+  templateUrl: 'checkout.component.html',
+  selector: 'eg-patron-checkout'
+})
+export class CheckoutComponent implements OnInit {
+
+    constructor(
+        private org: OrgService,
+        private net: NetService,
+        public patronService: PatronService,
+        public context: PatronManagerService
+    ) {}
+
+    ngOnInit() {
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/edit-toolbar.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/edit-toolbar.component.html
new file mode 100644 (file)
index 0000000..6a1647b
--- /dev/null
@@ -0,0 +1,7 @@
+<div class="row pb-2 pt-2">
+  <div class="ml-auto">
+    <button class="btn btn-outline-dark" i18n>Print</button>
+    <button class="btn btn-outline-dark ml-3" i18n>Save</button>
+    <button class="btn btn-outline-dark ml-3" i18n>Save &amp; Clone</button>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/edit-toolbar.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/edit-toolbar.component.ts
new file mode 100644 (file)
index 0000000..c538ab7
--- /dev/null
@@ -0,0 +1,25 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+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} from './patron.service';
+
+@Component({
+  templateUrl: 'edit-toolbar.component.html',
+  selector: 'eg-patron-edit-toolbar'
+})
+export class EditToolbarComponent implements OnInit {
+
+    constructor(
+        private org: OrgService,
+        private net: NetService,
+        public patronService: PatronService,
+        public context: PatronManagerService
+    ) {}
+
+    ngOnInit() {
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.html
new file mode 100644 (file)
index 0000000..139597f
--- /dev/null
@@ -0,0 +1,2 @@
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.ts
new file mode 100644 (file)
index 0000000..6101229
--- /dev/null
@@ -0,0 +1,25 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+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} from './patron.service';
+
+@Component({
+  templateUrl: 'edit.component.html',
+  selector: 'eg-patron-edit'
+})
+export class EditComponent implements OnInit {
+
+    constructor(
+        private org: OrgService,
+        private net: NetService,
+        public patronService: PatronService,
+        public context: PatronManagerService
+    ) {}
+
+    ngOnInit() {
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.css b/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.css
new file mode 100644 (file)
index 0000000..d4abd5f
--- /dev/null
@@ -0,0 +1,5 @@
+
+::ng-deep legend {
+  font-size: 1rem; /* defaults to 1.5 */
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.html
new file mode 100644 (file)
index 0000000..55b619c
--- /dev/null
@@ -0,0 +1,174 @@
+
+<ng-container *ngIf="!context.patron">
+  <eg-staff-banner bannerText="Manage Patrons" i18n-bannerText>
+  </eg-staff-banner>
+</ng-container>
+
+<ng-container *ngIf="context.patron">
+  <eg-staff-banner i18n-bannerText bannerText="
+    {{patronService.namePart(context.patron, 'family_name')}}, 
+    {{patronService.namePart(context.patron, 'first_given_name')}} 
+    {{patronService.namePart(context.patron, 'second_given_name')}}">
+  </eg-staff-banner>
+</ng-container>
+
+<div class="row">
+
+  <ng-container *ngIf="showSummaryPane()">
+    <div class="col-lg-3">
+      <div class="sticky-top-with-nav bg-white">
+        <ng-container *ngIf="context.patron">
+          <eg-patron-summary></eg-patron-summary>
+        </ng-container>
+      </div>
+    </div>
+  </ng-container>
+
+  <div [ngClass]="{'col-lg-9': showSummaryPane(), 'col-lg-12': !showSummaryPane()}">
+
+    <div class="sticky-top-with-nav bg-white">
+      <ul ngbNav #patronNav="ngbNav" class="nav-tabs"
+        [activeId]="patronTab" (navChange)="beforeTabChange($event)">
+
+        <ng-container *ngIf="patronTab !== 'search'">
+          <li ngbDropdown ngbNavItem="toggle">
+            <a href class="nav-link" (click)="toggleSummaryPane(); false"
+              title="Toggle Summary Pane" i18n-title>
+              <ng-container *ngIf="showSummaryPane()">
+                <span class="material-icons">navigate_before</span>
+              </ng-container>
+              <ng-container *ngIf="!showSummaryPane()">
+                <span class="material-icons">navigate_next</span>
+              </ng-container>
+            </a>
+          </li>
+        </ng-container>
+
+        <li ngbNavItem="checkout" [disabled]="!context.patron">
+          <a ngbNavLink i18n>Checkout</a>
+          <ng-template ngbNavContent>
+            <div class="">
+              <eg-patron-checkout></eg-patron-checkout> 
+            </div>
+          </ng-template>
+        </li>
+
+        <li ngbNavItem="items_out" [disabled]="!context.patron">
+          <a ngbNavLink i18n>Items Out</a>
+          <ng-template ngbNavContent>
+            <div class="">
+            </div>
+          </ng-template>
+        </li>
+
+        <li ngbNavItem="holds" [disabled]="!context.patron">
+          <a ngbNavLink i18n>Holds</a>
+          <ng-template ngbNavContent>
+            <eg-holds-grid [recordId]="recordId"
+              preFetchSetting="eg.circ.patron.holds.prefetch"
+              printTemplate="holds_for_patron"
+              persistKey="circ.patron.holds"
+              [hidePickupLibFilter]="true"
+              [defaultSort]="[{name:'request_time',dir:'asc'}]"
+              [patronId]="patronId"></eg-holds-grid>
+          </ng-template>
+        </li>
+
+        <li ngbNavItem="bills" [disabled]="!context.patron">
+          <a ngbNavLink i18n>Bills</a>
+          <ng-template ngbNavContent>
+            <div class="">
+            </div>
+          </ng-template>
+        </li>
+
+        <li ngbNavItem="messages" [disabled]="!context.patron">
+          <a ngbNavLink i18n>Messages</a>
+          <ng-template ngbNavContent>
+            <div class="">
+            </div>
+          </ng-template>
+        </li>
+
+        <li ngbNavItem="edit" [disabled]="!context.patron">
+          <a ngbNavLink i18n>Edit</a>
+          <ng-template ngbNavContent>
+            <eg-patron-edit 
+              contentPaneClass="patron-content-pane div-scroll-vert">
+            </eg-patron-edit> 
+          </ng-template>
+        </li>
+
+        <li ngbDropdown ngbNavItem="other" [disabled]="!context.patron">
+          <a [attr.href]="context.patron ? '' : null" 
+            (click)="false" class="nav-link" ngbDropdownToggle>Other</a>
+          <div ngbDropdownMenu>
+            <a routerLink="/staff/circ/patron/{{patronId}}/alerts" 
+              ngbDropdownItem i18n>Alerts</a>
+            <a routerLink="/staff/circ/patron/{{patronId}}/notes" 
+              ngbDropdownItem i18n>Notes</a>
+            <a routerLink="/staff/circ/patron/{{patronId}}/triggered_events" 
+              ngbDropdownItem i18n>Triggered Events / Notifications</a>
+            <a routerLink="/staff/circ/patron/{{patronId}}/message_center" 
+              ngbDropdownItem i18n>Message Center</a>
+            <a routerLink="/staff/circ/patron/{{patronId}}/stat_cats"
+              ngbDropdownItem i18n>Statistical Categories</a>
+            <a routerLink="/staff/circ/patron/{{patronId}}/surveys"
+              ngbDropdownItem i18n>Surveys</a>
+            <a routerLink="/staff/circ/patron/{{patronId}}/group"
+              ngbDropdownItem i18n>Group Member Details</a>
+            <a routerLink="/staff/circ/patron/{{patronId}}/edit_perms"
+              ngbDropdownItem i18n>User Permission Editor</a>
+            <a routerLink="/staff/circ/patron/{{patronId}}/credentials"
+              ngbDropdownItem i18n>Test Password</a>
+            <a href="/eg/staff/acq/requests/user/{{patronId}}" 
+              target="_top"
+              ngbDropdownItem i18n>Acquisition Patron Requests</a>
+            <a routerLink="/staff/booking/manage_reservations/by_patron/{{patronId}}"
+              target="_top"
+              ngbDropdownItem i18n>Booking: Manage Reservations</a>
+            <a routerLink="/staff/booking/create_reservation/for_patron/{{patronId}}"
+              target="_top"
+              ngbDropdownItem i18n>Booking: Create Reservation</a>
+            <a routerLink="/staff/booking/pickup/by_patron/{{patronId}}"
+              target="_top"
+              ngbDropdownItem i18n>Booking: Pick Up Reservations</a>
+            <a routerLink="/staff/booking/return/by_patron/{{patronId}}"
+              target="_top"
+              ngbDropdownItem i18n></a>
+            <a href (click)="purgeAccount(); false"
+              [disabled]="disablePurge()"
+              ngbDropdownItem i18n>Completely Purge Account</a>
+          </div>
+          <ng-template ngbNavContent>
+            <!-- display selected altTab component -->
+            OTHER STUFF: {{altTab}}
+          </ng-template>
+        </li>
+
+        <li ngbNavItem="search" class="ml-auto">
+          <a ngbNavLink i18n>Patron Search</a>
+          <ng-template ngbNavContent>
+            <div class="">
+              <eg-patron-search
+                (selectionChange)="patronSelectionChange($event)"
+                (patronsActivated)="patronsActivated($event)">
+              </eg-patron-search> 
+            </div>
+          </ng-template>
+        </li>
+      </ul>
+
+      <ng-container *ngIf="patronTab === 'edit'">
+        <!-- put the editor toolbar up here in the sticky section -->
+        <eg-patron-edit-toolbar></eg-patron-edit-toolbar>
+      </ng-container>
+
+    </div><!-- end of sticky top -->
+
+    <div *ngIf="!loading" class="pt-3">
+      <div [ngbNavOutlet]="patronNav"></div>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.ts
new file mode 100644 (file)
index 0000000..e65b8d2
--- /dev/null
@@ -0,0 +1,155 @@
+import {Component, OnInit, AfterViewInit} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {PatronService} from '@eg/staff/share/patron/patron.service';
+import {PatronManagerService} from './patron.service';
+import {PatronSearchComponent} from '@eg/staff/share/patron/search.component';
+
+const MAIN_TABS =
+    ['checkout', 'items_out', 'holds', 'bills', 'messages', 'edit', 'search'];
+
+@Component({
+  templateUrl: 'patron.component.html',
+  styleUrls: ['patron.component.css']
+})
+export class PatronComponent implements OnInit, AfterViewInit {
+
+    patronId: number;
+    patronTab = 'search';
+    altTab: string;
+    showSummary = true;
+    loading = true;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private auth: AuthService,
+        private store: ServerStoreService,
+        public patronService: PatronService,
+        public context: PatronManagerService
+    ) {}
+
+    ngOnInit() {
+        this.watchForTabChange();
+        this.load();
+    }
+
+    load() {
+        this.loading = true;
+        this.fetchSettings()
+        .then(_ => this.loading = false);
+    }
+
+    fetchSettings(): Promise<any> {
+
+        return this.store.getItemBatch([
+            'eg.circ.patron.summary.collapse'
+        ]).then(prefs => {
+            this.showSummary = !prefs['eg.circ.patron.summary.collapse'];
+        });
+    }
+
+    watchForTabChange() {
+        this.route.paramMap.subscribe((params: ParamMap) => {
+            this.patronTab = params.get('tab') || 'search';
+            this.patronId = +params.get('id');
+
+            if (MAIN_TABS.includes(this.patronTab)) {
+                this.altTab = null;
+            } else {
+                this.altTab = this.patronTab;
+                this.patronTab = 'other';
+            }
+
+            const prevId =
+                this.context.patron ? this.context.patron.id() : null;
+
+            if (this.patronId) {
+                if (this.patronId !== prevId) { // different patron
+                    this.context.loadPatron(this.patronId);
+                }
+            } else {
+                // Use the ID of the previously loaded patron.
+                this.patronId = prevId;
+            }
+        });
+    }
+
+    ngAfterViewInit() {
+    }
+
+    beforeTabChange(evt: NgbNavChangeEvent) {
+        // tab will change with route navigation.
+        evt.preventDefault();
+
+        this.patronTab = evt.nextId;
+        this.routeToTab();
+    }
+
+    routeToTab() {
+        let url = '/staff/circ/patron/';
+
+        switch (this.patronTab) {
+            case 'search':
+            case 'bcsearch':
+                url += this.patronTab;
+                break;
+            case 'other':
+                url += `${this.patronId}/${this.altTab}`;
+                break;
+            default:
+                url += `${this.patronId}/${this.patronTab}`;
+        }
+
+        this.router.navigate([url]);
+    }
+
+    showSummaryPane(): boolean {
+        return this.showSummary || this.patronTab === 'search';
+    }
+
+    toggleSummaryPane() {
+        this.store.setItem( // collapse is the opposite of show
+            'eg.circ.patron.summary.collapse', this.showSummary);
+        this.showSummary = !this.showSummary;
+    }
+
+    // Patron row single-clicked in the grid.  Load the patron without
+    // leaving the search tab.
+    patronSelectionChange(ids: number[]) {
+        if (ids.length !== 1) { return; }
+
+        const id = ids[0];
+        if (id !== this.patronId) {
+            this.patronId = id;
+            this.context.loadPatron(id);
+            return;
+        }
+    }
+
+    // Route to checkout tab for selected patron.
+    patronsActivated(rows: any[]) {
+        if (rows.length !== 1) { return; }
+
+        const id = rows[0].id();
+        this.patronId = id;
+        this.patronTab = 'checkout';
+        this.routeToTab();
+    }
+
+    disablePurge(): boolean {
+        return
+            this.context.patron.super_user() === 't' ||
+            this.patronId === this.auth.user().id();
+    }
+
+    purgeAccount() {
+        // show scary warning, etc.
+
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts
new file mode 100644 (file)
index 0000000..76cd7dc
--- /dev/null
@@ -0,0 +1,41 @@
+import {NgModule} from '@angular/core';
+import {PatronRoutingModule} from './routing.module';
+import {FmRecordEditorModule} from '@eg/share/fm-editor/fm-editor.module';
+import {StaffCommonModule} from '@eg/staff/common.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 {PatronManagerService} from './patron.service';
+import {PatronComponent} from './patron.component';
+import {SummaryComponent} from './summary.component';
+import {CheckoutComponent} from './checkout.component';
+import {EditComponent} from './edit.component';
+import {EditToolbarComponent} from './edit-toolbar.component';
+import {BcSearchComponent} from './bcsearch.component';
+
+@NgModule({
+  declarations: [
+    PatronComponent,
+    SummaryComponent,
+    CheckoutComponent,
+    EditComponent,
+    EditToolbarComponent,
+    BcSearchComponent
+  ],
+  imports: [
+    StaffCommonModule,
+    FmRecordEditorModule,
+    HoldsModule,
+    HoldingsModule,
+    BookingModule,
+    PatronModule,
+    PatronRoutingModule
+  ],
+  providers: [
+    PatronManagerService
+  ]
+})
+
+export class PatronManagerModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.service.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.service.ts
new file mode 100644 (file)
index 0000000..ef7c004
--- /dev/null
@@ -0,0 +1,102 @@
+import {Injectable} from '@angular/core';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PatronService} from '@eg/staff/share/patron/patron.service';
+
+const PATRON_FLESH_FIELDS = [
+    'card',
+    'cards',
+    'settings',
+    'standing_penalties',
+    'addresses',
+    'billing_address',
+    'mailing_address',
+    'stat_cat_entries',
+    'waiver_entries',
+    'usr_activity',
+    'notes',
+    'profile',
+    'net_access_level',
+    'ident_type',
+    'ident_type2',
+    'groups'
+];
+
+interface PatronStats {
+    fines: {balance_owed: number};
+    checkouts: {
+        overdue: number,
+        claims_returned: number,
+        lost: number,
+        out: number,
+        total_out: number,
+        long_overdue: number
+    };
+}
+
+@Injectable()
+export class PatronManagerService {
+
+    patron: IdlObject;
+    patronStats: PatronStats;
+
+    // Value for YAOUS circ.do_not_tally_claims_returned
+    noTallyClaimsReturned = false;
+
+    // Value for YAOUS circ.tally_lost
+    tallyLost = false;
+
+    loaded = false;
+
+    constructor(
+        private net: NetService,
+        private auth: AuthService,
+        public patronService: PatronService
+    ) {}
+
+    loadPatron(id: number): Promise<any> {
+        this.loaded = false;
+        this.patron = null;
+
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.fleshed.retrieve',
+            this.auth.token(), id, PATRON_FLESH_FIELDS).toPromise()
+        .then(patron => this.patron = patron)
+        .then(_ => this.getPatronStats(id))
+        .then(_ => this.loaded = true);
+    }
+
+   getPatronStats(id: number): Promise<any> {
+
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.opac.vital_stats.authoritative',
+            this.auth.token(), id).toPromise()
+
+        .then((stats: PatronStats) => {
+
+            // force numeric values
+            stats.fines.balance_owed = Number(stats.fines.balance_owed);
+
+            Object.keys(stats.checkouts).forEach(key =>
+                stats.checkouts[key] = Number(stats.checkouts[key]));
+
+            stats.checkouts.total_out = stats.checkouts.out +
+                stats.checkouts.overdue + stats.checkouts.long_overdue
+
+            if (!this.noTallyClaimsReturned) {
+                stats.checkouts.total_out += stats.checkouts.claims_returned;
+            }
+
+            if (this.tallyLost) {
+                stats.checkouts.total_out += stats.checkouts.lost
+            }
+
+            return this.patronStats = stats;
+        });
+    }
+}
+
+
index a1b4ae6..999d5f7 100644 (file)
@@ -1,20 +1,29 @@
 import {NgModule} from '@angular/core';
 import {RouterModule, Routes} from '@angular/router';
+import {PatronComponent} from './patron.component';
 
-const routes: Routes = [
-  { path: 'bcsearch',
-    loadChildren: () =>
-      import('./bcsearch/bcsearch.module').then(m => m.BcSearchModule)
-  },
-  { path: 'event-log',
+const routes: Routes = [{
+    path: '',
+    pathMatch: 'full',
+    redirectTo: 'search'
+  }, {
+    path: 'event-log',
     loadChildren: () =>
       import('./event-log/event-log.module').then(m => m.EventLogModule)
-  }
-];
+  }, {
+    path: 'search',
+    component: PatronComponent
+  }, {
+    path: 'bcsearch',
+    component: PatronComponent
+  }, {
+    path: ':id/:tab',
+    component: PatronComponent,
+}];
 
 @NgModule({
   imports: [RouterModule.forChild(routes)],
   exports: [RouterModule]
 })
 
-export class CircPatronRoutingModule {}
+export class PatronRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/summary.component.css b/Open-ILS/src/eg2/src/app/staff/circ/patron/summary.component.css
new file mode 100644 (file)
index 0000000..71ba8a4
--- /dev/null
@@ -0,0 +1,6 @@
+
+.patron-summary-container .row:nth-child(odd) {
+  background-color: rgb(248, 248, 248);
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/summary.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/summary.component.html
new file mode 100644 (file)
index 0000000..d1d0eb7
--- /dev/null
@@ -0,0 +1,141 @@
+
+<div class="patron-summary-container">
+
+  <h3 *ngIf="context.patron" class="font-weight-bold" i18n>
+    {{patronService.namePart(context.patron, 'family_name')}}, 
+    {{patronService.namePart(context.patron, 'first_given_name')}} 
+    {{patronService.namePart(context.patron, 'second_given_name')}}
+  </h3>
+
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Profile</div>
+    <div class="col-lg-7">{{context.patron.profile().name()}}</div>
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Home Library</div>
+    <div class="col-lg-7">{{orgSn(context.patron.home_ou())}}</div>
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Net Access</div>
+    <div class="col-lg-7">{{context.patron.net_access_level().name()}}</div>
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Date of Birth</div>
+    <div class="col-lg-7">{{context.patron.dob() | date:'shortDate'}}</div>
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Parent/Guardian</div>
+    <div class="col-lg-7">{{context.patron.guardian()}}</div>
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Last Activity</div>
+    <div class="col-lg-7">
+      <ng-container *ngIf="context.patron.usr_activity()[0]">
+        {{context.patron.usr_activity()[0].event_time() | date:'shortDate'}}
+      </ng-container>
+    </div>
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Last Updated</div>
+    <div class="col-lg-7">{{context.patron.last_update_time() | date:'shortDate'}}</div>
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Create Date</div>
+    <div class="col-lg-7">{{context.patron.create_date() | date:'shortDate'}}</div>
+  </div>
+  <div class="row">
+    <div class="col-lg-5" i18n>Expire Date</div>
+    <div class="col-lg-7">{{context.patron.expire_date() | date:'shortDate'}}</div>
+  </div>
+
+  <hr class="m-1"/>
+
+  <ng-container *ngIf="context.patronStats">
+
+    <div class="row mb-1">
+      <div class="col-lg-5" i18n>Fines Owed</div>
+      <div class="col-lg-7">{{context.patronStats.fines.total_owed | currency}}</div>
+    </div>
+    <div class="row mb-1">
+      <div class="col-lg-5" i18n>Items Out</div>
+      <div class="col-lg-7">{{context.patronStats.checkouts.total_out}}</div>
+    </div>
+    <div class="row mb-1">
+      <div class="col-lg-5" i18n>Overdue</div>
+      <div class="col-lg-7">{{context.patronStats.checkouts.overdue}}</div>
+    </div>
+    <div class="row mb-1">
+      <div class="col-lg-5" i18n>Long Overdue</div>
+      <div class="col-lg-7">{{context.patronStats.checkouts.long_overdue}}</div>
+    </div>
+    <div class="row mb-1">
+      <div class="col-lg-5" i18n>Claimed Returned</div>
+      <div class="col-lg-7">{{context.patronStats.checkouts.claims_returned}}</div>
+    </div>
+    <div class="row mb-1">
+      <div class="col-lg-5" i18n>Lost</div>
+      <div class="col-lg-7">{{context.patronStats.checkouts.lost}}</div>
+    </div>
+    <div class="row mb-1">
+      <div class="col-lg-5" i18n>Non-Cataloged</div>
+      <div class="col-lg-7">XXXX</div>
+    </div>
+    <div class="row">
+      <div class="col-lg-5" i18n>Holds</div>
+      <div class="col-lg-7">XX / YY</div>
+    </div>
+
+    <hr class="m-1"/>
+  </ng-container>
+
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Card</div>
+    <div class="col-lg-7">
+      {{context.patron.card() ? context.patron.card().barcode() : ''}}
+    </div>
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Username</div>
+    <div class="col-lg-7">{{context.patron.usrname()}}</div>
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Day Phone</div>
+    <div class="col-lg-7">{{context.patron.day_phone()}}</div> 
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Evening Phone</div>
+    <div class="col-lg-7">{{context.patron.evening_phone()}}</div> 
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Other Phone</div>
+    <div class="col-lg-7">{{context.patron.other_phone()}}</div> 
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>ID1 </div>
+    <div class="col-lg-7">{{context.patron.ident_value()}}</div> 
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>ID2</div>
+    <div class="col-lg-7">{{context.patron.ident_value2()}}</div> 
+  </div>
+  <div class="row mb-1">
+    <div class="col-lg-5" i18n>Email</div>
+    <div class="col-lg-7">
+      <!-- TODO: mailto link -->
+      {{context.patron.email()}}
+    </div> 
+  </div>
+
+  <hr class="m-1"/>
+
+  <div class="row mb-1" *ngFor="let addr of context.patron.addresses()">
+    <div class="col-lg-12">
+      <fieldset>
+        <legend i18n>{{addr.address_type()}}</legend><!-- todo -->
+        <div i18n>{{addr.street1()}} {{addr.street2()}}</div>
+        <div i18n>{{addr.city()}}, {{addr.state()}} {{addr.post_code()}}</div>
+      </fieldset>
+      <!-- todo textarea for copy -->
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/summary.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/summary.component.ts
new file mode 100644 (file)
index 0000000..90da1f9
--- /dev/null
@@ -0,0 +1,31 @@
+import {Component, OnInit, Input} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+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} from './patron.service';
+
+@Component({
+  templateUrl: 'summary.component.html',
+  styleUrls: ['summary.component.css'],
+  selector: 'eg-patron-summary'
+})
+export class SummaryComponent implements OnInit {
+
+    constructor(
+        private org: OrgService,
+        private net: NetService,
+        public patronService: PatronService,
+        public context: PatronManagerService
+    ) {}
+
+    ngOnInit() {
+    }
+
+    orgSn(orgId: number): string {
+        const org = this.org.get(orgId);
+        return org ? org.shortname() : '';
+    }
+}
+
index 0e94cc5..54e0ae4 100644 (file)
@@ -4,7 +4,7 @@ import {RouterModule, Routes} from '@angular/router';
 const routes: Routes = [{
   path: 'patron',
   loadChildren: () =>
-    import('./patron/routing.module').then(m => m.CircPatronRoutingModule)
+    import('./patron/patron.module').then(m => m.PatronManagerModule)
 }, {
   path: 'item',
   loadChildren: () =>
index b25a446..f52e36f 100644 (file)
@@ -37,7 +37,7 @@
     <h3 i18n>Holds Count: {{holdsCount}}</h3>
 
     <div class="row" *ngIf="!hidePickupLibFilter">
-      <div class="col-lg-4">
+      <div class="col-lg-5">
         <div class="input-group">
           <div class="input-group-prepend">
             <div class="input-group-text" i18n>Pickup Library</div>
         [initialValue]="enablePreFetch" i18n-label label="Pre-Fetch All Holds">
       </eg-grid-toolbar-checkbox>
 
+      <ng-container *ngIf="preFetchSetting && !hopeless">
+        <eg-grid-toolbar-checkbox (onChange)="preFetchHolds($event)"
+          [initialValue]="enablePreFetch" i18n-label label="Pre-Fetch All Holds">
+        </eg-grid-toolbar-checkbox>
+      </ng-container>
+
       <eg-grid-toolbar-action
         i18n-label label="Show Hold Details" i18n-group group="Hold"
         (onClick)="showDetails($event)"></eg-grid-toolbar-action>
         i18-group group="Item" i18n-label label="Show in Catalog"
         (onClick)="showTitle($event)"></eg-grid-toolbar-action>
 
+      <eg-grid-toolbar-action
+        i18n-label label="Show Holds For Title" i18n-group group="Show"
+        (onClick)="showHoldsForTitle($event)"></eg-grid-toolbar-action>
+
       <eg-grid-column i18n-label label="Hold ID" path='id' [index]="true" datatype="id">
       </eg-grid-column>
 
index 7a27e5a..6d70c32 100644 (file)
@@ -1,4 +1,5 @@
 import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Location} from '@angular/common';
 import {Observable, Observer, of} from 'rxjs';
 import {IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
@@ -102,13 +103,20 @@ export class HoldsGridComponent implements OnInit {
         }
     }
 
-    _userId: number;
-    @Input() set userId(id: number) {
-        this._userId = id;
+    get recordId(): number {
+        return this._recordId;
+    }
+
+    _patronId: number;
+    @Input() set patronId(id: number) {
+        this._patronId = id;
         if (this.initDone) {
             this.holdsGrid.reload();
         }
     }
+    get patronId(): number {
+        return this._patronId;
+    }
 
     // Include holds canceled on or after the provided date.
     // If no value is passed, canceled holds are not displayed.
@@ -119,6 +127,9 @@ export class HoldsGridComponent implements OnInit {
             this.holdsGrid.reload();
         }
     }
+    get showCanceledSince(): Date {
+        return this._showCanceledSince;
+    }
 
     // Include holds fulfilled on or after hte provided date.
     // If no value is passed, fulfilled holds are not displayed.
@@ -129,6 +140,9 @@ export class HoldsGridComponent implements OnInit {
             this.holdsGrid.reload();
         }
     }
+    get showFulfilledSince(): Date {
+        return this._showFulfilledSince;
+    }
 
 
     cellTextGenerator: GridCellTextGenerator;
@@ -152,6 +166,7 @@ export class HoldsGridComponent implements OnInit {
     }
 
     constructor(
+        private ngLocation: Location,
         private net: NetService,
         private org: OrgService,
         private store: ServerStoreService,
@@ -301,12 +316,12 @@ export class HoldsGridComponent implements OnInit {
                 this.org.descendants(this.pickupLib, true);
         }
 
-        if (this._recordId) {
-            filters.record_id = this._recordId;
+        if (this.recordId) {
+            filters.record_id = this.recordId;
         }
 
-        if (this._userId) {
-            filters.usr_id = this._userId;
+        if (this.patronId) {
+            filters.usr_id = this.patronId;
         }
 
         return filters;
@@ -315,7 +330,7 @@ export class HoldsGridComponent implements OnInit {
     fetchHolds(pager: Pager, sort: any[]): Observable<any> {
 
         // We need at least one filter.
-        if (!this._recordId && !this.pickupLib && !this._userId && !this.pullListOrg) {
+        if (!this.recordId && !this.pickupLib && !this.patronId && !this.pullListOrg) {
             return of([]);
         }
 
@@ -396,6 +411,15 @@ export class HoldsGridComponent implements OnInit {
         this.showDetail(rows[0]);
     }
 
+    showHoldsForTitle(rows: any[]) {
+        if (rows.length === 0) { return; }
+
+        const url = this.ngLocation.prepareExternalUrl(
+            `/staff/catalog/record/${rows[0].record_id}/holds`);
+
+        window.open(url, '_blank');
+    }
+
     showDetail(row: any) {
         if (row) {
             this.mode = 'detail';
index 32aa678..d459b45 100644 (file)
@@ -50,5 +50,11 @@ export class PatronService {
         return this.pcrud.retrieve('au', id, pcrudOps).toPromise();
     }
 
+    // Returns a name part (e.g. family_name) with preference for
+    // preferred name value where available.
+    namePart(patron: IdlObject, part: string): string {
+        if (!patron) { return ''; }
+        return patron['pref_' + part]() || patron[part]();
+    }
 }
 
index 952cb42..fe8bcd8 100644 (file)
 <div class="patron-search-grid mt-4">
   <eg-grid #searchGrid idlClass="au" 
     persistKey="circ.patron.search"
-    (onRowActivate)="rowsSelected($event)"
-    (onRowClick)="rowsClicked($event)"
+    (rowSelectionChange)="gridSelectionChange($event)"
+    (onRowActivate)="rowsActivated($event)"
     [dataSource]="dataSource" 
     [showDeclaredFieldsOnly]="true"> 
 
index b2a4b5b..1d35a76 100644 (file)
@@ -36,11 +36,13 @@ export class PatronSearchComponent implements OnInit, AfterViewInit {
 
     @ViewChild('searchGrid', {static: false}) searchGrid: GridComponent;
 
-    // Fired on dbl-click of a search result row.
-    @Output() patronsSelected: EventEmitter<any>;
+    // Fires on dbl-click or Enter while one or more search result
+    // rows are selected.
+    @Output() patronsActivated: EventEmitter<any>;
 
-    // Fired on single click of a search results row
-    @Output() patronsClicked: EventEmitter<any>;
+    // Fires when the selection of search result rows changes.
+    // Emits an array of patron IDs
+    @Output() selectionChange: EventEmitter<number[]>;
 
     search: any = {};
     searchOrg: IdlObject;
@@ -55,8 +57,8 @@ export class PatronSearchComponent implements OnInit, AfterViewInit {
         private auth: AuthService,
         private store: ServerStoreService
     ) {
-        this.patronsSelected = new EventEmitter<any>();
-        this.patronsClicked = new EventEmitter<any>();
+        this.patronsActivated = new EventEmitter<any>();
+        this.selectionChange = new EventEmitter<number[]>();
         this.dataSource = new GridDataSource();
         this.dataSource.getRows = (pager: Pager, sort: any[]) => {
             return this.getRows(pager, sort);
@@ -93,12 +95,12 @@ export class PatronSearchComponent implements OnInit, AfterViewInit {
         }
     }
 
-    rowsSelected(rows: IdlObject | IdlObject[]) {
-        this.patronsSelected.emit([].concat(rows));
+    gridSelectionChange(keys: string[]) {
+        this.selectionChange.emit(keys.map(k => Number(k)));
     }
 
-    rowsClicked(rows: IdlObject | IdlObject[]) {
-        this.patronsClicked.emit([].concat(rows));
+    rowsActivated(rows: IdlObject | IdlObject[]) {
+        this.patronsActivated.emit([].concat(rows));
     }
 
     getSelected(): IdlObject[] {
index 4e1366e..08f6959 100644 (file)
@@ -163,6 +163,14 @@ a {
   font-size: 99%;
 }
 
+/* Items stick to the top of the page once scrolled past,
+ * leaving room above for the nav bar */
+.sticky-top-with-nav {
+  top: 48px;
+  position: sticky;
+  z-index: 1;
+}
+
 /* --------------------------------------------------------------------------
 /* Form Validation CSS - https://angular.io/guide/form-validation
  * TODO: these colors don't fit the EG color scheme
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.angular-patron.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.angular-patron.sql
new file mode 100644 (file)
index 0000000..c7b9bad
--- /dev/null
@@ -0,0 +1,24 @@
+
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version); 
+
+/*
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.catalog.results.count', 'gui', 'integer',
+    oils_i18n_gettext(
+        'eg.catalog.results.count',
+        'Catalog Results Page Size',
+        'cwst', 'label'
+    )
+);
+*/
+
+eg.circ.patron.holds.prefetch
+
+eg.grid.circ.patron.holds
+
+holds_for_patron print template
+
+COMMIT;