LP1840773 SCKO Angular
authorBill Erickson <berickxx@gmail.com>
Wed, 22 Jun 2022 20:18:17 +0000 (16:18 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 22 Jun 2022 20:18:17 +0000 (16:18 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/scko/banner.component.html
Open-ILS/src/eg2/src/app/scko/banner.component.ts
Open-ILS/src/eg2/src/app/scko/items.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/scko/items.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/scko/routing.module.ts
Open-ILS/src/eg2/src/app/scko/scko.component.css
Open-ILS/src/eg2/src/app/scko/scko.component.html
Open-ILS/src/eg2/src/app/scko/scko.module.ts
Open-ILS/src/eg2/src/app/scko/scko.service.ts
Open-ILS/src/eg2/src/app/scko/summary.component.html
Open-ILS/src/eg2/src/app/scko/summary.component.ts

index d8b1f9b..037360a 100644 (file)
               [(ngModel)]="patronUsername" name="patron-username"
               placeholder="Username or Barcode" i18n-placeholder>
 
-            <label class="sr-only" for="patron-password" i18n>Password</label>
-
-            <input type="password" class="form-control shadow border border-dark rounded ml-2" 
-              autocomplete="off" id="patron-password" required
-              [(ngModel)]="patronPassword" name="patron-password"
-              placeholder="Password" i18n-placeholder>
-
-            <button type="submit" class="btn btn-dark ml-2">Submit</button>
+            <ng-container *ngIf="scko.patronPasswordRequired">
+              <label class="sr-only" for="patron-password" i18n>Password</label>
+
+              <input type="password" class="form-control shadow border border-dark rounded ml-2" 
+                autocomplete="off" id="patron-password" required
+                [(ngModel)]="patronPassword" name="patron-password"
+                placeholder="Password" i18n-placeholder>
+            </ng-container>
           </form>
         </div>
         <div class="flex-1"></div>
@@ -48,7 +48,7 @@
             <input type="text" class="form-control border border-dark shadow-rounded" 
               autocomplete="off" id="item-barcode" required 
               [(ngModel)]="itemBarcode" name="item-barcode"
-              placeholder="Item Barcode" i18n-placeholder>
+              placeholder="Item Barcode..." i18n-placeholder>
 
           </form>
         </div>
index a7cc682..b6b311e 100644 (file)
@@ -42,6 +42,7 @@ export class SckoBannerComponent implements OnInit {
 
     ngOnInit() {
 
+        // TODO focus the right thing on page load
         const node = document.getElementById('staff-username');
 
         // NOTE: Displaying a list of workstations will not work for users
@@ -110,70 +111,13 @@ export class SckoBannerComponent implements OnInit {
 
     submitPatronLogin() {
         this.patronLoginFailed = false;
-        this.loadPatron().finally(() => {
+        this.scko.loadPatron(this.patronUsername, this.patronPassword).finally(() => {
             this.patronLoginFailed = this.scko.patronSummary === null;
         });
     }
 
-    loadPatron(): Promise<any> {
-        this.scko.resetPatron();
-
-        if (!this.patronUsername) { return; }
-
-        let username;
-        let barcode;
-
-        if (this.patronUsername.match(this.scko.barcodeRegex)) {
-            barcode = this.patronUsername;
-        } else {
-            username = this.patronUsername;
-        }
-
-        if (this.scko.patronPasswordRequired) {
-            // TODO verify password
-
-            return this.net.request(
-                'open-ils.actor',
-                'open-ils.actor.verify_user_password',
-                this.auth.token(), barcode, username, null, this.patronPassword)
-
-            .toPromise().then(verified => {
-                if (Number(verified) === 1) {
-                    return this.fetchPatron(username, barcode);
-                } else {
-                    return Promise.reject('Bad password');
-                }
-            });
-
-        } else {
-
-            return this.fetchPatron(username, barcode);
-        }
-    }
-
-    fetchPatron(username: string, barcode: string): Promise<any> {
-
-        return this.net.request(
-            'open-ils.actor',
-            'open-ils.actor.user.retrieve_id_by_barcode_or_username',
-            this.auth.token(), barcode, username).toPromise()
-
-        .then(patronId => {
-
-            const evt = this.evt.parse(patronId);
-
-            if (evt || !patronId) {
-                console.error('Cannot find user: ', evt);
-                return Promise.reject('User not found');
-            }
-
-            return this.scko.loadPatron(patronId);
-        });
-    }
-
     submitItemBarcode() {
         console.log('Submitting barcode ', this.itemBarcode);
     }
-
 }
 
diff --git a/Open-ILS/src/eg2/src/app/scko/items.component.html b/Open-ILS/src/eg2/src/app/scko/items.component.html
new file mode 100644 (file)
index 0000000..e861623
--- /dev/null
@@ -0,0 +1,33 @@
+<div id='oils-selfck-circ-table-div'>
+  <table id='oils-selfck-circ-table' class='oils-selfck-item-table'>
+    <thead>
+      <tr>
+        <td class="rounded-left" id='oils-self-circ-pic-cell'></td>
+        <td i18n>Barcode</td>
+        <td i18n>Title</td>
+        <td i18n>Author</td>
+        <td i18n>Due Date</td>
+        <td i18n>Renewals Left</td>
+        <td class="rounded-right" i18n>Type</td>
+      </tr>
+    </thead>
+    <tbody id='oils-selfck-circ-out-tbody' class='oils-selfck-item-table'>
+           <tr *ngFor="let circ of circs">
+        <td>
+          <ng-container *ngIf="circ.target_copy().id() != -1">
+            <img src="/opac/extras/ac/jacket/small/r/{{circ.target_copy().call_number().record().id()}}"/>
+          </ng-container>
+        </td>
+        <td>{{circ.target_copy().barcode()}}</td>
+        <td>{{getTitle(circ)}}</td>
+        <td>{{getAuthor(circ)}}</td>
+        <td>{{circ | egDueDate}}</td>
+        <td>{{circ.renewal_remaining()}}</td>
+        <td>
+          <span *ngIf="circ.parent_circ()" i18n>Renewal</span>
+          <span *ngIf="!circ.parent_circ()" i18n>Checkout</span>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/scko/items.component.ts b/Open-ILS/src/eg2/src/app/scko/items.component.ts
new file mode 100644 (file)
index 0000000..54a9186
--- /dev/null
@@ -0,0 +1,89 @@
+import {Component, OnInit, ViewEncapsulation} from '@angular/core';
+import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
+import {tap} from 'rxjs/operators';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {NetService} from '@eg/core/net.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {SckoService} from './scko.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+
+const CIRC_FLESH_DEPTH = 4;
+const CIRC_FLESH_FIELDS = {
+  circ: ['target_copy'],
+  acp:  ['call_number'],
+  acn:  ['record'],
+  bre:  ['flat_display_entries']
+};
+
+@Component({
+  templateUrl: 'items.component.html'
+})
+
+export class SckoItemsComponent implements OnInit {
+
+    circs: IdlObject[] = [];
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        public  scko: SckoService
+    ) {}
+
+    ngOnInit() {
+
+        if (!this.scko.patronSummary) {
+            this.router.navigate(['/scko']);
+            return;
+        }
+
+        this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.checked_out.authoritative',
+            this.auth.token(), this.scko.patronSummary.id).toPromise()
+
+        .then(data => {
+            const ids = data.out.concat(data.overdue).concat(data.long_overdue);
+
+                       return this.pcrud.search('circ', {id: ids}, {
+                               flesh: CIRC_FLESH_DEPTH,
+                               flesh_fields: CIRC_FLESH_FIELDS,
+                               order_by : {circ : 'due_date'},
+
+                               select: {bre : ['id']}
+
+                       }).pipe(tap(circ => {
+                               this.circs.push(circ);
+                       })).toPromise();
+        });
+    }
+
+    isPrecat(copy: IdlObject): boolean {
+        return Number(copy.id()) === -1;
+    }
+
+    displayValue(circ: IdlObject, field: string): string {
+
+        const entry =
+            circ.target_copy().call_number().record().flat_display_entries()
+            .filter(e => e.name() === field)[0];
+
+        return entry ? entry.value() : '';
+    }
+
+    getTitle(circ: IdlObject): string {
+        const copy = circ.target_copy();
+        if (this.isPrecat(copy)) { return copy.dummy_title(); }
+        return this.displayValue(circ, 'title');
+    }
+
+    getAuthor(circ: IdlObject): string {
+        const copy = circ.target_copy();
+        if (this.isPrecat(copy)) { return copy.dummy_author(); }
+        return this.displayValue(circ, 'author');
+    }
+}
+
index 4c9ed2a..02e4930 100644 (file)
@@ -2,6 +2,7 @@ import {NgModule} from '@angular/core';
 import {RouterModule, Routes} from '@angular/router';
 import {SckoComponent} from './scko.component';
 import {SckoCheckoutComponent} from './checkout.component';
+import {SckoItemsComponent} from './items.component';
 
 const routes: Routes = [{
   path: '',
@@ -9,6 +10,9 @@ const routes: Routes = [{
   children: [{
     path: '',
     component: SckoCheckoutComponent
+  }, {
+    path: 'items',
+    component: SckoItemsComponent
   }]
 }];
 
index 0ccf92c..c034896 100644 (file)
@@ -90,7 +90,7 @@ A {
 
 .oils-selfck-item-table td {
     text-align: left;
-    padding: 7px;
+    padding: 10px;
 }
 
 .oils-selfck-item-table thead {
index a273be6..45df3e9 100644 (file)
@@ -2,12 +2,12 @@
 <eg-scko-banner></eg-scko-banner>
 
 <div *ngIf="scko.auth.token() && scko.patronSummary" class="row mt-5">
-  <div class="col-lg-8">
+  <div class="col-lg-9">
     <div class="ml-2 scko-page">
       <router-outlet></router-outlet>
     </div>
   </div>
-  <div class="col-lg-4"><eg-scko-summary></eg-scko-summary></div>
+  <div class="col-lg-3"><eg-scko-summary></eg-scko-summary></div>
 </div>
 
 <!-- global toast alerts -->
index e4b795d..b3823c4 100644 (file)
@@ -11,6 +11,7 @@ import {SckoService} from './scko.service';
 import {SckoBannerComponent} from './banner.component';
 import {SckoSummaryComponent} from './summary.component';
 import {SckoCheckoutComponent} from './checkout.component';
+import {SckoItemsComponent} from './items.component';
 
 @NgModule({
   declarations: [
@@ -18,6 +19,7 @@ import {SckoCheckoutComponent} from './checkout.component';
     SckoBannerComponent,
     SckoSummaryComponent,
     SckoCheckoutComponent,
+    SckoItemsComponent,
   ],
   imports: [
     EgCommonModule,
index a82e819..e1b6c3a 100644 (file)
@@ -1,10 +1,11 @@
 import {Injectable, EventEmitter} from '@angular/core';
+import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
+import {OrgService} from '@eg/core/org.service';
 import {NetService} from '@eg/core/net.service';
 import {AuthService} from '@eg/core/auth.service';
 import {EventService, EgEvent} from '@eg/core/event.service';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
 import {StoreService} from '@eg/core/store.service';
-import {ServerStoreService} from '@eg/core/server-store.service';
 import {PatronService, PatronSummary, PatronStats} from '@eg/staff/share/patron/patron.service';
 
 @Injectable({providedIn: 'root'})
@@ -18,9 +19,10 @@ export class SckoService {
     sessionCheckouts: any[] = [];
 
     constructor(
+        private route: ActivatedRoute,
+        private org: OrgService,
         private net: NetService,
         private evt: EventService,
-        private serverStore: ServerStoreService,
         public auth: AuthService,
         private patrons: PatronService,
     ) {}
@@ -37,7 +39,9 @@ export class SckoService {
 
         .then(_ => {
 
-            return this.serverStore.getItemBatch([
+            // Note we cannot use server-store unless we are logged
+            // in with a workstation.
+            return this.org.settings([
                 'opac.barcode_regex',
                 'circ.selfcheck.patron_login_timeout',
                 'circ.selfcheck.auto_override_checkout_events',
@@ -55,13 +59,65 @@ export class SckoService {
             this.barcodeRegex = new RegExp(regPattern);
             this.patronPasswordRequired =
                 sets['circ.selfcheck.patron_password_required'];
-            console.log('REQ', this.patronPasswordRequired);
+
+            // Load a patron by barcode via URL params.
+            // Useful for development.
+            const username = this.route.snapshot.queryParamMap.get('patron');
+
+            if (username && !this.patronPasswordRequired) {
+                return this.loadPatron(username);
+            }
         });
     }
 
-    loadPatron(id: number): Promise<any> {
-        return this.patrons.getFleshedById(id).then(
-            patron => this.patronSummary = new PatronSummary(patron))
+    loadPatron(username: string, password?: string): Promise<any> {
+        this.resetPatron();
+
+        if (!username) { return; }
+
+        let barcode;
+        if (username.match(this.barcodeRegex)) {
+            barcode = username;
+            username = null;
+        }
+
+        if (!this.patronPasswordRequired) {
+            return this.fetchPatron(username, barcode);
+        }
+
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.verify_user_password',
+            this.auth.token(), barcode, username, null, password)
+
+        .toPromise().then(verified => {
+            if (Number(verified) === 1) {
+                return this.fetchPatron(username, barcode);
+            } else {
+                return Promise.reject('Bad password');
+            }
+        });
+    }
+
+    fetchPatron(username: string, barcode: string): Promise<any> {
+
+        return this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.user.retrieve_id_by_barcode_or_username',
+            this.auth.token(), barcode, username).toPromise()
+
+        .then(patronId => {
+
+            const evt = this.evt.parse(patronId);
+
+            if (evt || !patronId) {
+                console.error('Cannot find user: ', evt);
+                return Promise.reject('User not found');
+            }
+
+            return this.patrons.getFleshedById(patronId);
+        })
+        .then(patron => this.patronSummary = new PatronSummary(patron))
         .then(_ => this.patrons.getVitalStats(this.patronSummary.patron))
         .then(stats => this.patronSummary.stats = stats);
     }
@@ -71,10 +127,13 @@ export class SckoService {
     }
 
     accountTotalCheckouts(): number {
+        // stats.checkouts.total_out includes claims returned
+
         return this.sessionTotalCheckouts() +
-            this.patronSummary.stats.checkouts.total_out;
+            this.patronSummary.stats.checkouts.out +
+            this.patronSummary.stats.checkouts.overdue +
+            this.patronSummary.stats.checkouts.long_overdue;
     }
-
 }
 
 
index d6e06fd..20d22b0 100644 (file)
     <legend i18n>Items Checked Out</legend>
     <div>
       <span i18n>Total items this session: </span>
-      <span>{{scko.sessionTotalCheckouts()}}</span>
+      <span class="font-weight-bold">{{scko.sessionTotalCheckouts()}}</span>
     </div>
     <div class="mt-2">
       <span i18n>Total items on account: </span>
-      <span>{{scko.accountTotalCheckouts()}}</span>
+      <span class="font-weight-bold">{{scko.accountTotalCheckouts()}}</span>
     </div>
     <div class="mt-2">
-      <a (click)="null">
+      <a routerLink="/scko/items">
         <button type="button" class="scko-button" i18n>View Items Out</button>
       </a>
     </div>
   <fieldset>
     <legend i18n>Holds</legend>
     <div i18n>
-      You have {{scko.patronSummary.stats.holds.ready}} item(s) ready for pickup
+      You have 
+      <span class="font-weight-bold">{{scko.patronSummary.stats.holds.ready}}</span> 
+      item(s) ready for pickup.
     </div>
     <div class="mt-2" i18n>
-      You have {{scko.patronSummary.stats.holds.total}} total holds
+      You have 
+      <span class="font-weight-bold">{{scko.patronSummary.stats.holds.total}}</span>
+      total holds.
     </div>
     <div class="mt-2">
       <a href='javascript:void(0);' id='oils-selfck-hold-details-link'>
   </fieldset>
   <fieldset>
     <legend i18n>Fines</legend>
-    <div>{{scko.patronSummary.stats.fines.balance_owed | currency}}</div>
+    <div>
+      <span i18n>Total fines on account:</span>
+      <span class="font-weight-bold">
+        {{scko.patronSummary.stats.fines.balance_owed | currency}}
+      </span>
+    </div>
     <div class="mt-2">
       <span>
         <a href='javascript:void(0);' id='oils-selfck-view-fines-link'>
index 4baaff1..a1f153e 100644 (file)
@@ -18,13 +18,6 @@ export class SckoSummaryComponent implements OnInit {
     receiptType = 'email';
 
     constructor(
-        private route: ActivatedRoute,
-        private store: StoreService,
-        private net: NetService,
-        private auth: AuthService,
-        private evt: EventService,
-        private ngLocation: Location,
-        private org: OrgService,
         public scko: SckoService
     ) {}