LP1904036 Patron alerts page; resolver service
authorBill Erickson <berickxx@gmail.com>
Thu, 25 Feb 2021 22:26:08 +0000 (17:26 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:24 +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>
15 files changed:
Open-ILS/src/eg2/src/app/staff/circ/patron/alerts.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/alerts.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/edit-toolbar.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/holds.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.service.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/resolver.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.html
Open-ILS/src/eg2/src/app/staff/circ/patron/summary.component.ts

diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/alerts.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/alerts.component.html
new file mode 100644 (file)
index 0000000..6b869cb
--- /dev/null
@@ -0,0 +1,75 @@
+
+<div *ngIf="context.alerts">
+
+  <img class="mt-n4" src="/images/stop_sign.png"/>
+
+  <div class="alert alert-info" *ngIf="context.alerts.holdsReady > 0" i18n>
+    Holds available: {{context.alerts.holdsReady}}
+  </div>
+
+  <div class="mt-2 alert alert-warning" *ngIf="context.alerts.accountExpired" i18n>
+    Patron account is EXPIRED.
+  </div>
+
+  <div class="mt-2 alert alert-warning" *ngIf="context.alerts.accountExpiresSoon" i18n>
+    Patron account will expire soon.  Please renew.
+  </div>
+
+  <div class="mt-2 alert alert-danger" *ngIf="context.alerts.patronBarred" i18n>
+    Patron account is BARRED
+  </div>
+
+  <div class="mt-2 alert alert-warning" *ngIf="context.alerts.patronInactive" i18n>
+    Patron account is INACTIVE
+  </div>
+
+  <div class="mt-2 alert alert-warning" *ngIf="context.alerts.retrievedWithInactive" i18n>
+    Patron account retrieved with an INACTIVE card.
+  </div>
+
+  <div class="mt-2 alert alert-warning" *ngIf="context.alerts.invalidAddress" i18n>
+    Patron account has invalid addresses.
+  </div>
+
+  <!-- alert message -->
+  <div class="row" *ngIf="context.alerts.alertMessage">
+    <div class="col-lg-6 offset-lg-3">
+      <div class="card">
+        <div class="card-header" i18n>Alert Message</div>
+        <div class="card-body">{{context.alerts.alertMessage}}</div>
+      </div>
+    </div>
+  </div>
+
+  <!-- penalties -->
+  <div class="row" *ngIf="context.alerts.alertPenalties.length">
+    <div class="col-lg-12">
+      <div class="card">
+        <div class="card-header" i18n>Penalties</div>
+        <div class="card-body">
+          <ul class="list-group list-group-flush">
+            <li class="list-group-item" 
+              *ngFor="let penalty of context.alerts.alertPenalties">
+              <div class="row">
+                <div class="col-lg-2">
+                  {{context.orgSn(penalty.org_unit())}}
+                </div>
+                <div class="col-lg-8"
+                  title="{{penalty.standing_penalty().name()}}">
+                  {{penalty.standing_penalty().label()}}
+                  <div>{{penalty.note()}}</div><!-- force newline -->
+                </div>
+                <div class="col-lg-2">
+                  {{penalty.set_date() | date:'short'}}
+                </div>
+              </div>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="mt-4 well-value" i18n>
+    Select a tab above (for example, Check Out) to clear this alert.
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/alerts.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/alerts.component.ts
new file mode 100644 (file)
index 0000000..814bdca
--- /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 {PatronContextService} from './patron.service';
+
+@Component({
+  templateUrl: 'alerts.component.html',
+  selector: 'eg-patron-alerts'
+})
+export class PatronAlertsComponent implements OnInit {
+
+    constructor(
+        private org: OrgService,
+        private net: NetService,
+        public patronService: PatronService,
+        public context: PatronContextService
+    ) {}
+
+    ngOnInit() {
+    }
+}
+
index b1c73ca..62aacad 100644 (file)
@@ -7,7 +7,7 @@ import {IdlObject} from '@eg/core/idl.service';
 import {OrgService} from '@eg/core/org.service';
 import {NetService} from '@eg/core/net.service';
 import {PatronService} from '@eg/staff/share/patron/patron.service';
-import {PatronManagerService, CircGridEntry} from './patron.service';
+import {PatronContextService, CircGridEntry} from './patron.service';
 import {CheckoutParams, CheckoutResult, CircService
     } from '@eg/staff/share/circ/circ.service';
 import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
@@ -59,7 +59,7 @@ export class CheckoutComponent implements OnInit, AfterViewInit {
         private net: NetService,
         public circ: CircService,
         public patronService: PatronService,
-        public context: PatronManagerService,
+        public context: PatronContextService,
         private audio: AudioService
     ) {}
 
index c538ab7..39d180d 100644 (file)
@@ -4,7 +4,7 @@ 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';
+import {PatronContextService} from './patron.service';
 
 @Component({
   templateUrl: 'edit-toolbar.component.html',
@@ -16,7 +16,7 @@ export class EditToolbarComponent implements OnInit {
         private org: OrgService,
         private net: NetService,
         public patronService: PatronService,
-        public context: PatronManagerService
+        public context: PatronContextService
     ) {}
 
     ngOnInit() {
index 6101229..18092ec 100644 (file)
@@ -4,7 +4,7 @@ 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';
+import {PatronContextService} from './patron.service';
 
 @Component({
   templateUrl: 'edit.component.html',
@@ -16,7 +16,7 @@ export class EditComponent implements OnInit {
         private org: OrgService,
         private net: NetService,
         public patronService: PatronService,
-        public context: PatronManagerService
+        public context: PatronContextService
     ) {}
 
     ngOnInit() {
index 79bfae6..e7ed104 100644 (file)
@@ -4,7 +4,7 @@ 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';
+import {PatronContextService} from './patron.service';
 
 @Component({
   templateUrl: 'holds.component.html',
@@ -16,7 +16,7 @@ export class HoldsComponent implements OnInit {
         private org: OrgService,
         private net: NetService,
         public patronService: PatronService,
-        public context: PatronManagerService
+        public context: PatronContextService
     ) {}
 
     ngOnInit() {
index 692d727..170f84f 100644 (file)
@@ -9,7 +9,7 @@ import {NetService} from '@eg/core/net.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {AuthService} from '@eg/core/auth.service';
 import {PatronService} from '@eg/staff/share/patron/patron.service';
-import {PatronManagerService} from './patron.service';
+import {PatronContextService} from './patron.service';
 import {CheckoutResult, CircService} from '@eg/staff/share/circ/circ.service';
 import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
 import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
@@ -57,7 +57,7 @@ export class ItemsComponent implements OnInit, AfterViewInit {
         private store: StoreService,
         private serverStore: ServerStoreService,
         public patronService: PatronService,
-        public context: PatronManagerService
+        public context: PatronContextService
     ) {}
 
     ngOnInit() {
index 7362069..798148c 100644 (file)
@@ -45,7 +45,7 @@
         </ng-container>
 
         <li ngbNavItem="checkout" [disabled]="!context.patron">
-          <a ngbNavLink i18n>Checkout</a>
+          <a ngbNavLink i18n>Check Out</a>
           <ng-template ngbNavContent>
             <div class="">
               <eg-patron-checkout></eg-patron-checkout> 
@@ -99,7 +99,7 @@
             (click)="false" class="nav-link" ngbDropdownToggle>Other</a>
           <div ngbDropdownMenu>
             <a routerLink="/staff/circ/patron/{{patronId}}/alerts" 
-              ngbDropdownItem i18n>Alerts</a>
+              ngbDropdownItem i18n>Alerts and Messages</a>
             <a routerLink="/staff/circ/patron/{{patronId}}/notes" 
               ngbDropdownItem i18n>Notes</a>
             <a routerLink="/staff/circ/patron/{{patronId}}/triggered_events" 
               ngbDropdownItem i18n>Completely Purge Account</a>
           </div>
           <ng-template ngbNavContent>
-            <!-- display selected altTab component -->
-            OTHER STUFF: {{altTab}}
+            <ng-container [ngSwitch]="altTab">
+              <div *ngSwitchCase="'alerts'">
+                <eg-patron-alerts></eg-patron-alerts>
+              </div>
+            </ng-container>
           </ng-template>
         </li>
 
index 2e2f089..b1b18cf 100644 (file)
@@ -5,7 +5,7 @@ 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 {PatronContextService} from './patron.service';
 import {PatronSearch, PatronSearchComponent
     } from '@eg/staff/share/patron/search.component';
 
@@ -31,7 +31,7 @@ export class PatronComponent implements OnInit, AfterViewInit {
         private auth: AuthService,
         private store: ServerStoreService,
         public patronService: PatronService,
-        public context: PatronManagerService
+        public context: PatronContextService
     ) {}
 
     ngOnInit() {
@@ -71,7 +71,7 @@ export class PatronComponent implements OnInit, AfterViewInit {
 
             if (this.patronId) {
                 if (this.patronId !== prevId) { // different patron
-                    this.context.loadPatron(this.patronId);
+                    this.changePatron(this.patronId);
                 }
             } else {
                 // Use the ID of the previously loaded patron.
@@ -126,12 +126,22 @@ export class PatronComponent implements OnInit, AfterViewInit {
 
         const id = ids[0];
         if (id !== this.patronId) {
-            this.patronId = id;
-            this.context.loadPatron(id);
-            return;
+            this.changePatron(id);
         }
     }
 
+    changePatron(id: number) {
+        this.patronId = id;
+        this.context.loadPatron(id)
+        .then(_ => {
+            if (this.context.patron &&
+                this.context.alerts.hasAlerts() &&
+               !this.context.patronAlertsShown()) {
+               this.router.navigate(['/staff/circ/patron', id, 'alerts'])
+            }
+        });
+    }
+
     // Route to checkout tab for selected patron.
     patronsActivated(rows: any[]) {
         if (rows.length !== 1) { return; }
index b459def..8f4ed17 100644 (file)
@@ -1,5 +1,6 @@
 import {NgModule} from '@angular/core';
 import {PatronRoutingModule} from './routing.module';
+import {PatronResolver} from './resolver.service';
 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';
@@ -7,8 +8,9 @@ import {CircModule} from '@eg/staff/share/circ/circ.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 {PatronContextService} from './patron.service';
 import {PatronComponent} from './patron.component';
+import {PatronAlertsComponent} from './alerts.component';
 import {SummaryComponent} from './summary.component';
 import {CheckoutComponent} from './checkout.component';
 import {HoldsComponent} from './holds.component';
@@ -21,6 +23,7 @@ import {ItemsComponent} from './items.component';
 @NgModule({
   declarations: [
     PatronComponent,
+    PatronAlertsComponent,
     SummaryComponent,
     CheckoutComponent,
     HoldsComponent,
@@ -41,7 +44,8 @@ import {ItemsComponent} from './items.component';
     BarcodesModule
   ],
   providers: [
-    PatronManagerService
+    PatronResolver,
+    PatronContextService
   ]
 })
 
index 3e63f8b..4e6fcb4 100644 (file)
@@ -1,9 +1,11 @@
 import {Injectable} from '@angular/core';
 import {IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
 import {AuthService} from '@eg/core/auth.service';
 import {PatronService} from '@eg/staff/share/patron/patron.service';
 import {PatronSearch} from '@eg/staff/share/patron/search.component';
+import {StoreService} from '@eg/core/store.service';
 
 export interface CircGridEntry {
     title?: string;
@@ -52,30 +54,54 @@ interface PatronStats {
     };
 }
 
+export class PatronAlerts {
+    holdsReady = 0;
+    accountExpired = false;
+    accountExpiresSoon = false;
+    patronBarred = false;
+    patronInactive = false;
+    retrievedWithInactive = false;
+    invalidAddress = false;
+    alertMessage: string = null;
+    alertPenalties: IdlObject[] = [];
+
+    hasAlerts(): boolean {
+        return (
+            this.holdsReady > 0 ||
+            this.accountExpired ||
+            this.accountExpiresSoon ||
+            this.patronBarred ||
+            this.patronInactive ||
+            this.retrievedWithInactive ||
+            this.invalidAddress ||
+            this.alertMessage !== null ||
+            this.alertPenalties.length > 0
+        );
+    }
+}
+
 @Injectable()
-export class PatronManagerService {
+export class PatronContextService {
 
     patron: IdlObject;
     patronStats: PatronStats;
+    alerts: PatronAlerts;
 
-    // Value for YAOUS circ.do_not_tally_claims_returned
-    noTallyClaimsReturned = false;
-
-    // Value for YAOUS circ.tally_lost
-    tallyLost = false;
+    noTallyClaimsReturned = false; // circ.do_not_tally_claims_returned
+    tallyLost = false; // circ.tally_lost
 
     loaded = false;
 
-    accountExpired = false;
-    accountExpiresSoon = false;
-
     lastPatronSearch: PatronSearch;
+    searchBarcode: string = null;
 
     // These should persist tab changes
     checkouts: CircGridEntry[] = [];
 
     constructor(
+        private store: StoreService,
         private net: NetService,
+        private org: OrgService,
         private auth: AuthService,
         public patronService: PatronService
     ) {}
@@ -84,6 +110,7 @@ export class PatronManagerService {
         this.loaded = false;
         this.patron = null;
         this.checkouts = [];
+        this.alerts = new PatronAlerts();
 
         return this.net.request(
             'open-ils.actor',
@@ -91,30 +118,10 @@ export class PatronManagerService {
             this.auth.token(), id, PATRON_FLESH_FIELDS).toPromise()
         .then(p => this.patron = p)
         .then(_ => this.getPatronStats(id))
-        .then(_ => this.setExpires())
+        .then(_ => this.compileAlerts())
         .then(_ => this.loaded = true);
     }
 
-    setExpires(): Promise<any> {
-        this.accountExpired = false;
-        this.accountExpiresSoon = false;
-
-        // When quickly navigating patron search results it's possible
-        // for this.patron to be cleared right before this function
-        // is called.  Exit early instead of making an unneeded call.
-        // For this func. in particular a nasty JS error is thrown.
-        if (!this.patron) { return Promise.resolve(); }
-
-        return this.patronService.testExpire(this.patron)
-        .then(value => {
-            if (value === 'expired') {
-                this.accountExpired = true;
-            } else if (value === 'soon') {
-                this.accountExpiresSoon = true;
-            }
-        });
-    }
-
     getPatronStats(id: number): Promise<any> {
 
         // When quickly navigating patron search results it's possible
@@ -163,6 +170,50 @@ export class PatronManagerService {
             }
         });
     }
+
+    patronAlertsShown(): boolean {
+        if (!this.patron) { return false; }
+        const shown = this.store.getSessionItem('eg.circ.last_alerted_patron');
+        if (shown === this.patron.id()) { return true; }
+        this.store.setSessionItem('eg.circ.last_alerted_patron', this.patron.id());
+        return false;
+    }
+
+    compileAlerts(): Promise<any> {
+
+        // User navigated to a different patron mid-data load.
+        if (!this.patron) { return Promise.resolve(); }
+
+        this.alerts.holdsReady = this.patronStats.holds.ready;
+        this.alerts.patronBarred = this.patron.barred() === 't';
+        this.alerts.patronInactive = this.patron.active() === 'f';
+        this.alerts.invalidAddress = this.patron.addresses()
+            .filter(a => a.valid() === 'f').length > 0;
+        this.alerts.alertMessage = this.patron.alert_message();
+        this.alerts.alertPenalties = this.patron.standing_penalties()
+            .filter(p => p.standing_penalty().staff_alert() === 't');
+
+        if (this.searchBarcode) {
+            const card = this.patron.cards()
+                .filter(c => c.barcode() === this.searchBarcode)[0];
+            this.alerts.retrievedWithInactive = card && card.active() === 'f';
+            this.searchBarcode = null;
+        }
+
+        return this.patronService.testExpire(this.patron)
+        .then(value => {
+            if (value === 'expired') {
+                this.alerts.accountExpired = true;
+            } else if (value === 'soon') {
+                this.alerts.accountExpiresSoon = true;
+            }
+        });
+    }
+
+    orgSn(orgId: number): string {
+        const org = this.org.get(orgId);
+        return org ? org.shortname() : '';
+    }
 }
 
 
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/resolver.service.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/resolver.service.ts
new file mode 100644 (file)
index 0000000..8ddef06
--- /dev/null
@@ -0,0 +1,40 @@
+import {Injectable} from '@angular/core';
+import {Router, Resolve, RouterStateSnapshot,
+        ActivatedRouteSnapshot} from '@angular/router';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PatronContextService} from './patron.service';
+
+
+@Injectable()
+export class PatronResolver implements Resolve<Promise<any[]>> {
+
+    constructor(
+        private store: ServerStoreService,
+        private context: PatronContextService
+    ) {}
+
+    resolve(
+        route: ActivatedRouteSnapshot,
+        state: RouterStateSnapshot): Promise<any[]> {
+
+        return this.fetchSettings();
+    }
+
+    fetchSettings(): Promise<any> {
+
+        return this.store.getItemBatch([
+          'eg.circ.patron.summary.collapse',
+          'circ.do_not_tally_claims_returned',
+          'circ.tally_lost'
+
+        ]).then(settings => {
+            this.context.noTallyClaimsReturned =
+                settings['circ.do_not_tally_claims_returned'];
+            this.context.tallyLost = settings['circ.tally_lost'];
+        });
+    }
+}
+
index de04742..06c95d8 100644 (file)
@@ -2,6 +2,7 @@ import {NgModule} from '@angular/core';
 import {RouterModule, Routes} from '@angular/router';
 import {PatronComponent} from './patron.component';
 import {BcSearchComponent} from './bcsearch.component';
+import {PatronResolver} from './resolver.service';
 
 const routes: Routes = [{
     path: '',
@@ -13,7 +14,8 @@ const routes: Routes = [{
       import('./event-log/event-log.module').then(m => m.EventLogModule)
   }, {
     path: 'search',
-    component: PatronComponent
+    component: PatronComponent,
+    resolve: {resolver : PatronResolver}
   }, {
     path: 'bcsearch',
     component: BcSearchComponent
@@ -21,8 +23,12 @@ const routes: Routes = [{
     path: 'bcsearch/:barcode',
     component: BcSearchComponent
   }, {
+    path: ':id',
+    redirectTo: ':id/checkout'
+  }, {
     path: ':id/:tab',
     component: PatronComponent,
+    resolve: {resolver : PatronResolver}
 }];
 
 @NgModule({
index cdeaf97..4b6e162 100644 (file)
@@ -19,7 +19,7 @@
   </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 class="col-lg-7">{{context.orgSn(context.patron.home_ou())}}</div>
   </div>
   <div class="row mb-1">
     <div class="col-lg-5" i18n>Net Access</div>
index 90da1f9..aac3c49 100644 (file)
@@ -4,7 +4,7 @@ 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';
+import {PatronContextService} from './patron.service';
 
 @Component({
   templateUrl: 'summary.component.html',
@@ -17,15 +17,10 @@ export class SummaryComponent implements OnInit {
         private org: OrgService,
         private net: NetService,
         public patronService: PatronService,
-        public context: PatronManagerService
+        public context: PatronContextService
     ) {}
 
     ngOnInit() {
     }
-
-    orgSn(orgId: number): string {
-        const org = this.org.get(orgId);
-        return org ? org.shortname() : '';
-    }
 }