--- /dev/null
+
+<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>
+
+
--- /dev/null
+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);
+ }
+}
+
+
+++ /dev/null
-
-<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>
-
-
+++ /dev/null
-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);
- }
-}
-
-
+++ /dev/null
-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 {}
-
+++ /dev/null
-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 {}
--- /dev/null
+
+CHECKOUT
--- /dev/null
+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() {
+ }
+}
+
--- /dev/null
+<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 & Clone</button>
+ </div>
+</div>
--- /dev/null
+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() {
+ }
+}
+
--- /dev/null
+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() {
+ }
+}
+
--- /dev/null
+
+::ng-deep legend {
+ font-size: 1rem; /* defaults to 1.5 */
+}
+
--- /dev/null
+
+<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>
+
--- /dev/null
+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.
+
+ }
+}
+
--- /dev/null
+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 {}
+
--- /dev/null
+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;
+ });
+ }
+}
+
+
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 {}
--- /dev/null
+
+.patron-summary-container .row:nth-child(odd) {
+ background-color: rgb(248, 248, 248);
+}
+
+
--- /dev/null
+
+<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>
--- /dev/null
+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() : '';
+ }
+}
+
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: () =>
<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>
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';
}
}
- _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.
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.
this.holdsGrid.reload();
}
}
+ get showFulfilledSince(): Date {
+ return this._showFulfilledSince;
+ }
cellTextGenerator: GridCellTextGenerator;
}
constructor(
+ private ngLocation: Location,
private net: NetService,
private org: OrgService,
private store: ServerStoreService,
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;
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([]);
}
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';
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]();
+ }
}
<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">
@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;
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);
}
}
- 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[] {
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
--- /dev/null
+
+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;