LP1840773 SCKO Angular Hatch Accessible
authorBill Erickson <berickxx@gmail.com>
Thu, 7 Jul 2022 14:55:15 +0000 (10:55 -0400)
committerBill Erickson <berickxx@gmail.com>
Thu, 14 Jul 2022 19:36:36 +0000 (15:36 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
37 files changed:
Open-ILS/src/eg2/src/app/routing.module.ts
Open-ILS/src/eg2/src/app/scko/banner.component.html [deleted file]
Open-ILS/src/eg2/src/app/scko/banner.component.ts [deleted file]
Open-ILS/src/eg2/src/app/scko/checkout.component.html [deleted file]
Open-ILS/src/eg2/src/app/scko/checkout.component.ts [deleted file]
Open-ILS/src/eg2/src/app/scko/fines.component.html [deleted file]
Open-ILS/src/eg2/src/app/scko/fines.component.ts [deleted file]
Open-ILS/src/eg2/src/app/scko/holds.component.html [deleted file]
Open-ILS/src/eg2/src/app/scko/holds.component.ts [deleted file]
Open-ILS/src/eg2/src/app/scko/items.component.html [deleted file]
Open-ILS/src/eg2/src/app/scko/items.component.ts [deleted file]
Open-ILS/src/eg2/src/app/scko/routing.module.ts [deleted file]
Open-ILS/src/eg2/src/app/scko/scko.component.css [deleted file]
Open-ILS/src/eg2/src/app/scko/scko.component.html [deleted file]
Open-ILS/src/eg2/src/app/scko/scko.component.ts [deleted file]
Open-ILS/src/eg2/src/app/scko/scko.module.ts [deleted file]
Open-ILS/src/eg2/src/app/scko/scko.service.ts [deleted file]
Open-ILS/src/eg2/src/app/scko/summary.component.html [deleted file]
Open-ILS/src/eg2/src/app/scko/summary.component.ts [deleted file]
Open-ILS/src/eg2/src/app/staff/scko/banner.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/banner.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/checkout.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/checkout.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/fines.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/fines.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/holds.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/holds.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/items.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/items.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/scko.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/scko.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/scko.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/scko.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/scko.service.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/summary.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/scko/summary.component.ts [new file with mode: 0644]

index 087c0cc..c440a3a 100644 (file)
@@ -18,9 +18,9 @@ const routes: Routes = [
     resolve : {startup : BaseResolver},
     loadChildren: () => import('./staff/staff.module').then(m => m.StaffModule)
   }, {
-    path: 'scko',
+    path: 'staff/scko',
     resolve : {startup : BaseResolver},
-    loadChildren: () => import('./scko/scko.module').then(m => m.SckoModule)
+    loadChildren: () => import('./staff/scko/scko.module').then(m => m.SckoModule)
   }
 ];
 
diff --git a/Open-ILS/src/eg2/src/app/scko/banner.component.html b/Open-ILS/src/eg2/src/app/scko/banner.component.html
deleted file mode 100644 (file)
index cea5bd0..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-
-<div id="scko-banner" class="pb-2">
-       <div id="scko-logo-div">
-               <img src="/images/self_eg_logo.png"/>
-       </div>
-  <div class="scko-scan-container mt-3">
-    <ng-container *ngIf="scko.auth.user() && !scko.patronSummary">
-      <div id="scko-scan-input-text" i18n>
-        Please log in with your username or library barcode.
-      </div>
-      <div class="d-flex mt-3 mb-3">
-        <div class="flex-1"></div>
-        <div>
-          <form (ngSubmit)="submitPatronLogin()" #patronLoginForm="ngForm"
-            autocomplete="off" class="form-validated form-inline">
-
-            <label class="sr-only" for="patron-username" i18n>Username</label>
-
-            <input type="text" class="form-control border border-dark shadow-rounded" 
-              autocomplete="off" id="patron-username" required 
-              [(ngModel)]="patronUsername" name="patron-username"
-              placeholder="Username or Barcode" i18n-placeholder>
-
-            <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>
-      </div>
-    </ng-container>
-
-    <ng-container *ngIf="scko.patronSummary">
-      <div id="scko-scan-input-text" i18n>Please enter an item barcode</div>
-      <div class="d-flex mt-3 mb-3">
-        <div class="flex-1"></div>
-        <div>
-          <form (ngSubmit)="submitItemBarcode()" #barcodeForm="ngForm"
-            autocomplete="off" class="form-validated form-inline">
-
-            <label class="sr-only" for="item-barcode" i18n>Item Barcode</label>
-
-            <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>
-
-          </form>
-        </div>
-        <div class="flex-1 d-flex">
-          <div class="flex-1"></div>
-          <div id="scko-welcome-message" class="mr-2 rounded" i18n>Welcome,
-            {{scko.patronSummary.patron.pref_first_given_name() 
-              || scko.patronSummary.patron.first_given_name()}}
-          </div>
-        </div>
-      </div>
-    </ng-container>
-
-  </div>
-</div>
-
-<div *ngIf="!scko.auth.user()" class="container mt-3">
-
-  <div class="col-lg-6 offset-lg-3">
-    <fieldset>
-      <legend class="mb-0" i18n><h1>Staff Account Login</h1></legend>
-      <hr class="mt-1"/>
-      <form (ngSubmit)="submitStaffLogin()" #staffLoginForm="ngForm" class="form-validated">
-
-        <div class="form-group row">
-          <label class="col-lg-4 text-right font-weight-bold" 
-            for="staff-username" i18n>Username</label>
-          <input 
-            type="text" 
-            class="form-control col-lg-8"
-            id="staff-username" 
-            name="staff-username"
-            required
-            autocomplete="username"
-            i18n-placeholder
-            placeholder="Staff Username" 
-            [(ngModel)]="staffUsername"/>
-        </div>
-
-        <div class="form-group row">
-          <label class="col-lg-4 text-right font-weight-bold" 
-            for="staff-password" i18n>Password</label>
-          <input 
-            type="password" 
-            class="form-control col-lg-8"
-            id="staff-password" 
-            name="staff-password"
-            required
-            autocomplete="current-password"
-            i18n-placeholder
-            placeholder="Staff Password" 
-            [(ngModel)]="staffPassword"/>
-        </div>
-
-        <div class="form-group row" *ngIf="workstations && workstations.length">
-          <label class="col-lg-4 text-right font-weight-bold" 
-            for="workstation" i18n>Workstation</label>
-          <select 
-            class="form-control col-lg-8" 
-            id="workstation" 
-            name="workstation"
-            required
-            [(ngModel)]="args.workstation">
-            <option *ngFor="let ws of workstations" [value]="ws.name">
-              {{ws.name}}
-            </option>
-          </select>
-        </div>
-
-        <div class="row">
-          <div class="col-lg-2 offset-lg-4 pl-0">
-            <button type="submit" class="btn btn-outline-dark" i18n>Sign In</button>
-          </div>
-          <div class="col-lg-4" *ngIf="loginFailed">
-            <div class="badge badge-warning p-2" i18n>Login Failed</div>
-          </div>
-        </div>
-      </form>
-    </fieldset>
-  </div>
-</div>
diff --git a/Open-ILS/src/eg2/src/app/scko/banner.component.ts b/Open-ILS/src/eg2/src/app/scko/banner.component.ts
deleted file mode 100644 (file)
index 4485f86..0000000
+++ /dev/null
@@ -1,142 +0,0 @@
-import {Component, OnInit, AfterViewInit, NgZone, HostListener} from '@angular/core';
-import {Location} from '@angular/common';
-import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
-import {AuthService, AuthWsState} from '@eg/core/auth.service';
-import {NetService} from '@eg/core/net.service';
-import {StoreService} from '@eg/core/store.service';
-import {SckoService} from './scko.service';
-import {OrgService} from '@eg/core/org.service';
-import {EventService, EgEvent} from '@eg/core/event.service';
-
-@Component({
-  selector: 'eg-scko-banner',
-  templateUrl: 'banner.component.html'
-})
-
-export class SckoBannerComponent implements OnInit, AfterViewInit {
-
-    workstations: any[];
-    workstationNotFound = false;
-
-    patronUsername: string;
-    patronPassword: string;
-    patronLoginFailed = false;
-
-    staffUsername: string;
-    staffPassword: string;
-    staffWorkstation: string;
-    staffLoginFailed = false;
-
-    itemBarcode: string;
-
-    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
-    ) {}
-
-    ngOnInit() {
-
-        // NOTE: Displaying a list of workstations will not work for users
-        // of Hatch until the extension is updated to support /eg2/*/scko
-        this.store.getWorkstations()
-        .then(wsList => {
-            this.workstations = wsList;
-            return this.store.getDefaultWorkstation();
-        }).then(def => {
-            this.staffWorkstation = def;
-            this.applyWorkstation();
-        });
-    }
-
-    ngAfterViewInit() {
-        if (this.auth.token()) {
-            this.focusNode('patron-username');
-        } else {
-            this.focusNode('staff-username');
-        }
-
-        this.scko.focusBarcode.subscribe(_ => this.focusNode('item-barcode'));
-    }
-
-    focusNode(id: string) {
-        setTimeout(() => {
-            const node = document.getElementById(id);
-            if (node) { (node as HTMLInputElement).select(); }
-        });
-    }
-
-    applyWorkstation() {
-        const wanted = this.route.snapshot.queryParamMap.get('workstation');
-        if (!wanted) { return; } // use the default
-
-        const exists = this.workstations.filter(w => w.name === wanted)[0];
-        if (exists) {
-            this.staffWorkstation = wanted;
-        } else {
-            console.error(`Unknown workstation requested: ${wanted}`);
-        }
-    }
-
-    submitStaffLogin() {
-
-        this.staffLoginFailed = false;
-
-        const args = {
-            type: 'persistent',
-            username: this.staffUsername,
-            password: this.staffPassword,
-            workstation: this.staffWorkstation
-        };
-
-        this.staffLoginFailed = false;
-        this.workstationNotFound = false;
-
-        this.auth.login(args).then(
-            ok => {
-
-                if (this.auth.workstationState === AuthWsState.NOT_FOUND_SERVER) {
-                    this.staffLoginFailed = true;
-                    this.workstationNotFound = true;
-
-                } else {
-
-                    // Initial login clears cached org unit setting values
-                    // and user/workstation setting values
-                    this.org.clearCachedSettings().then(_ => {
-
-                        // Force reload of the app after a successful login.
-                        window.location.href =
-                            this.ngLocation.prepareExternalUrl('/scko');
-
-                    });
-                }
-            },
-            notOk => {
-                this.staffLoginFailed = true;
-            }
-        );
-    }
-
-    submitPatronLogin() {
-        this.patronLoginFailed = false;
-        this.scko.loadPatron(this.patronUsername, this.patronPassword).finally(() => {
-            if (this.scko.patronSummary === null) {
-                this.patronLoginFailed = true;
-            } else {
-                this.focusNode('item-barcode');
-            }
-        });
-    }
-
-    submitItemBarcode() {
-        this.scko.resetPatronTimeout();
-        this.scko.checkout(this.itemBarcode);
-    }
-}
-
diff --git a/Open-ILS/src/eg2/src/app/scko/checkout.component.html b/Open-ILS/src/eg2/src/app/scko/checkout.component.html
deleted file mode 100644 (file)
index 51f549f..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-<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 co of scko.sessionCheckouts">
-        <td>
-          <ng-container *ngIf="co.circ">
-            <img src="/opac/extras/ac/jacket/small/r/{{co.circ.target_copy().call_number().record().id()}}"/>
-          </ng-container>
-        </td>
-        <td><span *ngIf="co.circ">{{co.circ.target_copy().barcode()}}</span></td>
-        <td>{{scko.getCircTitle(co.circ)}}</td>
-        <td>{{scko.getCircAuthor(co.circ)}}</td>
-        <td><span *ngIf="co.circ">{{co.circ | egDueDate}}</span></td>
-        <td><span *ngIf="co.circ">{{co.circ.renewal_remaining()}}</span></td>
-        <td>
-          <ng-container *ngIf="co.circ">
-            <span *ngIf="co.circ.parent_circ()" i18n>Renewal</span>
-            <span *ngIf="!co.circ.parent_circ()" i18n>Checkout</span>
-          </ng-container>
-        </td>
-      </tr>
-    </tbody>
-  </table>
-</div>
diff --git a/Open-ILS/src/eg2/src/app/scko/checkout.component.ts b/Open-ILS/src/eg2/src/app/scko/checkout.component.ts
deleted file mode 100644 (file)
index 5afc1f6..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-import {Component, OnInit, ViewEncapsulation} from '@angular/core';
-import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
-import {AuthService} from '@eg/core/auth.service';
-import {IdlObject} from '@eg/core/idl.service';
-import {SckoService} from './scko.service';
-import {ServerStoreService} from '@eg/core/server-store.service';
-
-@Component({
-  templateUrl: 'checkout.component.html'
-})
-
-export class SckoCheckoutComponent implements OnInit {
-
-    constructor(
-        private router: Router,
-        private route: ActivatedRoute,
-        public  scko: SckoService
-    ) {}
-
-    ngOnInit() {
-    }
-}
-
diff --git a/Open-ILS/src/eg2/src/app/scko/fines.component.html b/Open-ILS/src/eg2/src/app/scko/fines.component.html
deleted file mode 100644 (file)
index d4444c3..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-<div class="d-flex">
-  <div class="flex-1"></div>
-  <div>
-    <button class="btn btn-outline-dark" (click)="printList()" i18n>Print List</button>
-  </div>
-</div>
-<div id='oils-selfck-circ-table-div'>
-  <table id='oils-selfck-circ-table' class='oils-selfck-item-table'>
-    <thead>
-      <tr>
-        <td i18n>Type</td>
-        <td i18n>Details</td>
-        <td i18n>Total Billed</td>
-        <td i18n>Total Paid</td>
-        <td i18n>Balance Owed</td>
-      </tr>
-    </thead>
-    <tbody class='oils-selfck-item-table'>
-           <tr *ngFor="let xact of xacts">
-        <td>
-          <ng-container *ngIf="xact.summary().xact_type() == 'circulation'" i18n>
-            Circulation
-          </ng-container>
-          <ng-container *ngIf="xact.summary().xact_type() != 'circulation'" i18n>
-            Miscellaneous
-          </ng-container>
-        </td>
-        <td>{{getDetails(xact)}}</td>
-        <td>{{xact.summary().total_owed() | currency}}</td>
-        <td>{{xact.summary().total_paid() | currency}}</td>
-        <td>{{xact.summary().balance_owed() | currency}}</td>
-      </tr>
-    </tbody>
-  </table>
-</div>
diff --git a/Open-ILS/src/eg2/src/app/scko/fines.component.ts b/Open-ILS/src/eg2/src/app/scko/fines.component.ts
deleted file mode 100644 (file)
index f13bda7..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-import {Component, OnInit, ViewEncapsulation} from '@angular/core';
-import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
-import {switchMap, 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 {PrintService} from '@eg/share/print/print.service';
-
-
-@Component({
-  templateUrl: 'fines.component.html'
-})
-
-export class SckoFinesComponent implements OnInit {
-
-    xacts: IdlObject[] = [];
-
-    constructor(
-        private router: Router,
-        private route: ActivatedRoute,
-        private net: NetService,
-        private auth: AuthService,
-        private pcrud: PcrudService,
-        private printer: PrintService,
-        public  scko: SckoService
-    ) {}
-
-    ngOnInit() {
-
-        if (!this.scko.patronSummary) {
-            this.router.navigate(['/scko']);
-            return;
-        }
-
-        this.scko.resetPatronTimeout();
-
-        this.pcrud.search('mbts',
-            {   usr: this.scko.patronSummary.id,
-                xact_finish: null,
-                balance_owed: {'<>' : 0}
-            }, {}, {atomic: true}
-        ).pipe(switchMap(sums => {
-
-            return this.pcrud.search('mbt', {id: sums.map(s => s.id())},
-                {   order_by: 'xact_start',
-                    flesh: 5,
-                    flesh_fields: {
-                        mbt: ['summary', 'circulation', 'grocery'],
-                        circ: ['target_copy'],
-                        acp: ['call_number'],
-                        acn: ['record'],
-                        bre: ['flat_display_entries']
-                    },
-                    select: {bre : ['id']}
-                }
-            ).pipe(tap(xact => this.xacts.push(xact)));
-        })).toPromise();
-    }
-
-    displayValue(xact: IdlObject, field: string): string {
-        const entry =
-            xact.circulation().target_copy().call_number().record().flat_display_entries()
-            .filter(e => e.name() === field)[0];
-
-        return entry ? entry.value() : '';
-    }
-
-    getTitle(xact: IdlObject): string {
-        const copy = xact.circulation().target_copy();
-
-        if (Number(copy.call_number().id()) === -1) {
-            return copy.dummy_title();
-        }
-
-        return this.displayValue(xact, 'title');
-    }
-
-    getDetails(xact: IdlObject): string {
-        if (xact.summary().xact_type() === 'circulation') {
-            return this.getTitle(xact);
-        } else {
-            return xact.summary().last_billing_type();
-        }
-    }
-
-    printList() {
-
-        const data = this.xacts.map(x => {
-            return {
-                xact: x, // full object if needed
-                details: this.getDetails(x),
-                total_owed: x.summary().total_owed(),
-                total_paid: x.summary().total_paid(),
-                balance_owed: x.summary().balance_owed(),
-            };
-        });
-
-        this.printer.print({
-            templateName: 'scko_fines',
-            contextData: {
-                xacts: data,
-                user: this.scko.patronSummary.patron
-            },
-            printContext: 'default'
-        });
-    }
-}
-
diff --git a/Open-ILS/src/eg2/src/app/scko/holds.component.html b/Open-ILS/src/eg2/src/app/scko/holds.component.html
deleted file mode 100644 (file)
index ff10e9b..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<div class="d-flex">
-  <div class="flex-1"></div>
-  <div>
-    <button class="btn btn-outline-dark" (click)="printList()" i18n>Print List</button>
-  </div>
-</div>
-<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>Title</td>
-        <td i18n>Author</td>
-        <td class="rounded-right" i18n>Status</td>
-      </tr>
-    </thead>
-    <tbody class='oils-selfck-item-table'>
-           <tr *ngFor="let hold of holds">
-        <td><img src="/opac/extras/ac/jacket/small/r/{{hold.record_id}}"/></td>
-        <td>{{hold.title}}</td>
-        <td>{{hold.author}}</td>
-        <td>
-          <ng-container *ngIf="hold.hold_status == 4" i18n>
-            Ready for Pickup
-          </ng-container>
-          <ng-container *ngIf="hold.hold_status != 4" i18n>
-            #{{hold.relative_queue_position}} in line with {{hold.potentials}} copie(s)
-          </ng-container>
-        </td>
-      </tr>
-    </tbody>
-  </table>
-</div>
diff --git a/Open-ILS/src/eg2/src/app/scko/holds.component.ts b/Open-ILS/src/eg2/src/app/scko/holds.component.ts
deleted file mode 100644 (file)
index 131cf73..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-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';
-import {PrintService} from '@eg/share/print/print.service';
-
-@Component({
-  templateUrl: 'holds.component.html'
-})
-
-export class SckoHoldsComponent implements OnInit {
-
-    holds: IdlObject[] = [];
-
-    constructor(
-        private router: Router,
-        private route: ActivatedRoute,
-        private net: NetService,
-        private auth: AuthService,
-        private pcrud: PcrudService,
-        private printer: PrintService,
-        public  scko: SckoService
-    ) {}
-
-    ngOnInit() {
-
-        if (!this.scko.patronSummary) {
-            this.router.navigate(['/scko']);
-            return;
-        }
-
-        this.scko.resetPatronTimeout();
-
-        const orderBy = [
-           {shelf_time: {nulls: 'last'}},
-           {capture_time: {nulls: 'last'}},
-           {request_time: {nulls: 'last'}}
-        ];
-
-        const filters = {
-            usr_id: this.scko.patronSummary.id,
-            fulfillment_time: null
-        };
-
-        let first = true;
-        this.net.request(
-            'open-ils.circ',
-            'open-ils.circ.hold.wide_hash.stream',
-            this.auth.token(), filters, orderBy, 1000, 0, {}
-        ).subscribe(holdData => {
-
-            if (first) { // First response is the hold count.
-                first = false;
-                return;
-            }
-
-            this.holds.push(holdData);
-        });
-    }
-
-    printList() {
-        this.printer.print({
-            templateName: 'scko_holds',
-            contextData: {
-              holds: this.holds,
-              user: this.scko.patronSummary.patron
-            },
-            printContext: 'default'
-        });
-    }
-}
-
-
diff --git a/Open-ILS/src/eg2/src/app/scko/items.component.html b/Open-ILS/src/eg2/src/app/scko/items.component.html
deleted file mode 100644 (file)
index c4a247b..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<div class="d-flex">
-  <div class="flex-1"></div>
-  <div>
-    <button class="btn btn-outline-dark" (click)="printList()" i18n>Print List</button>
-  </div>
-</div>
-<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>{{scko.getCircTitle(circ)}}</td>
-        <td>{{scko.getCircAuthor(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
deleted file mode 100644 (file)
index 7a662a5..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-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';
-import {PrintService} from '@eg/share/print/print.service';
-
-@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,
-        private printer: PrintService,
-        public  scko: SckoService
-    ) {}
-
-    ngOnInit() {
-
-        if (!this.scko.patronSummary) {
-            this.router.navigate(['/scko']);
-            return;
-        }
-
-        this.scko.resetPatronTimeout();
-
-        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.scko.getFleshedCircs(ids).pipe(tap(circ => {
-                this.circs.push(circ);
-            })).toPromise();
-        });
-    }
-
-    printList() {
-
-        const data = this.circs.map(c => {
-            return {
-                circ: c,
-                copy: c.target_copy(),
-                title: this.scko.getCircTitle(c),
-                author: this.scko.getCircAuthor(c)
-            };
-        });
-
-        this.printer.print({
-            templateName: 'scko_items_out',
-            contextData: {
-                checkouts: data,
-                user: this.scko.patronSummary.patron
-            },
-            printContext: 'default'
-        });
-    }
-}
-
-
-
diff --git a/Open-ILS/src/eg2/src/app/scko/routing.module.ts b/Open-ILS/src/eg2/src/app/scko/routing.module.ts
deleted file mode 100644 (file)
index 0881453..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-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';
-import {SckoHoldsComponent} from './holds.component';
-import {SckoFinesComponent} from './fines.component';
-
-const routes: Routes = [{
-  path: '',
-  component: SckoComponent,
-  children: [{
-    path: '',
-    component: SckoCheckoutComponent
-  }, {
-    path: 'items',
-    component: SckoItemsComponent
-   }, {
-    path: 'holds',
-    component: SckoHoldsComponent
-   }, {
-    path: 'fines',
-    component: SckoFinesComponent
- }]
-}];
-
-@NgModule({
-  imports: [RouterModule.forChild(routes)],
-  exports: [RouterModule]
-})
-
-export class SckoRoutingModule {}
-
diff --git a/Open-ILS/src/eg2/src/app/scko/scko.component.css b/Open-ILS/src/eg2/src/app/scko/scko.component.css
deleted file mode 100644 (file)
index 0150ee3..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-body {                                                                         
-    font-family: Arial, Verdana;                                               
-    font-size: 13px;                                                           
-}                                                                              
-
-A {
-    text-decoration: none;
-}
-                                                                               
-#scko-banner {                                                         
-    background: #00593d; /* Old browsers */                                    
-/* IE9 SVG, needs conditional override of 'filter' to 'none' */                
-    background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwNTkzZCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMwMDdhNTQiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+);
-    background: -moz-linear-gradient(top,  #00593d 0%, #007a54 100%); /* FF3.6+ */
-    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00593d), color-stop(100%,#007a54)); /* Chrome,Safari4+ */
-    background: -webkit-linear-gradient(top,  #00593d 0%,#007a54 100%); /* Chrome10+,Safari5.1+ */
-    background: -o-linear-gradient(top,  #00593d 0%,#007a54 100%); /* Opera 11.10+ */
-    background: -ms-linear-gradient(top,  #00593d 0%,#007a54 100%); /* IE10+ */
-    background: linear-gradient(to bottom,  #00593d 0%,#007a54 100%); /* W3C */
-    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00593d', endColorstr='#007a54',GradientType=0 ); /* IE6-8 */
-    padding-top: 20px;                                                         
-    text-align: center;                                                        
-    font-weight:bold;                                                          
-}  
-
-#scko-scan-input-text {                                                       
-  font-size: 16px;                                                           
-  background: none repeat scroll 0 0 #252525;                                
-  color: white;                                                              
-  padding: 10px;                                                             
-} 
-
-#scko-welcome-message {
-  font-size: 16px;                                                           
-  background: none repeat scroll 0 0 #252525;                                
-  color: white;                                                              
-  padding: 10px;                                                             
-}
-
-#scko-circ-info-div fieldset {
-    margin: 15px 10px 15px 10px;
-    padding: 8px;
-    background: #ffffff; /* Old browsers */
-    /* IE9 SVG, needs conditional override of 'filter' to 'none' */
-    background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9Ijk4JSIgc3RvcC1jb2xvcj0iI2UwZTBlMCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=);
-    background: -moz-linear-gradient(top,  #ffffff 0%, #e0e0e0 98%); /* FF3.6+ */
-    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(98%,#e0e0e0)); /* Chrome,Safari4+ */
-    background: -webkit-linear-gradient(top,  #ffffff 0%,#e0e0e0 98%); /* Chrome10+,Safari5.1+ */
-    background: -o-linear-gradient(top,  #ffffff 0%,#e0e0e0 98%); /* Opera 11.10+ */
-    background: -ms-linear-gradient(top,  #ffffff 0%,#e0e0e0 98%); /* IE10+ */
-    background: linear-gradient(to bottom,  #ffffff 0%,#e0e0e0 98%); /* W3C */
-    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#e0e0e0',GradientType=0 ); /* IE6-8 */
-    border: 1px solid #DDDDDD;
-    color: #333333;
-    -webkit-border-radius: 5px;
-    border-radius: 5px;
-}
-
-#scko-circ-info-div fieldset legend {
-    font-size: 13px;
-    color: #00593d;
-    background-color: white;
-    font-weight: bold;
-}
-
-.scko-button { 
-    color: #FBF9F9;
-    font-weight: bold;
-    letter-spacing: 1px;
-    font-size: 94%;
-    text-shadow: 1px 1px 1px #555555;
-    cursor: pointer !important;
-    border-radius: 12px;
-    border: 1px solid #007a54;
-    background: linear-gradient(#007a54, #00593d);
-    background: -moz-linear-gradient(#007a54, #00593d);
-    background: -o-linear-gradient(#007a54, #00593d);
-    background: -webkit-linear-gradient(#007a54, #00593d);
-    background-color: #00593d;
-    padding: 5px 10px 6px;
-    outline: 0 none;
-    text-decoration: none;
-}
-
-
-.oils-selfck-item-table  {
-    width: 98%;
-    margin-top: 15px;
-}
-
-.oils-selfck-item-table td {
-    text-align: left;
-    padding: 10px;
-}
-
-.oils-selfck-item-table thead {
-    font-weight: bold;
-    color: #00593D;
-}
-
-.oils-selfck-item-table tbody tr {
-    border-bottom: 1px solid #888;
-}
-
-.scko-page {
-    margin-top: 20px;
-    background: #ffffff; /* Old browsers */
-    /* IE9 SVG, needs conditional override of 'filter' to 'none' */
-    background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9Ijk4JSIgc3RvcC1jb2xvcj0iI2UwZTBlMCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=);
-    background: -moz-linear-gradient(top,  #ffffff 0%, #e0e0e0 98%); /* FF3.6+ */
-    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(98%,#e0e0e0)); /* Chrome,Safari4+ */
-    background: -webkit-linear-gradient(top,  #ffffff 0%,#e0e0e0 98%); /* Chrome10+,Safari5.1+ */
-    background: -o-linear-gradient(top,  #ffffff 0%,#e0e0e0 98%); /* Opera 11.10+ */
-    background: -ms-linear-gradient(top,  #ffffff 0%,#e0e0e0 98%); /* IE10+ */
-    background: linear-gradient(to bottom,  #ffffff 0%,#e0e0e0 98%); /* W3C */
-    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#e0e0e0',GradientType=0 ); /* IE6-8 */
-    border: 1px solid #DDDDDD;
-    color: #333333;
-    -webkit-border-radius: 5px;
-    border-radius: 5px;
-    padding: 3px;
-}
-
-.scko-status-row {
-    font-size: 20px;
-}
diff --git a/Open-ILS/src/eg2/src/app/scko/scko.component.html b/Open-ILS/src/eg2/src/app/scko/scko.component.html
deleted file mode 100644 (file)
index 1ae367d..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-
-<eg-scko-banner></eg-scko-banner>
-
-<div class="d-flex scko-status-row mt-2">
-  <div class="flex-1"></div>
-  <div [ngClass]="{
-    'text-success': scko.statusDisplaySuccess, 
-    'text-danger': !scko.statusDisplaySuccess}">{{scko.statusDisplayText}}</div>
-  <div class="flex-1"></div>
-</div>
-
-<div *ngIf="scko.auth.token() && scko.patronSummary" class="row mr-0 mt-5">
-  <div class="col-lg-9">
-    <div class="ml-2 scko-page">
-      <router-outlet></router-outlet>
-    </div>
-  </div>
-  <div class="col-lg-3"><eg-scko-summary></eg-scko-summary></div>
-</div>
-
-<eg-confirm-dialog #logoutDialog 
-  i18n-dialogTitle i18n-dialogBody
-  i18n-confirmButtonText i18n-cancelButtonText
-  dialogTitle="Logout Notice"
-  dialogBody="Your login session will timeout due to inactivity"
-  confirmButtonText="Continue Session"
-  cancelButtonText="Logout">
-</eg-confirm-dialog>
-
-<eg-alert-dialog #alertDialog i18n-dialogTitle="Notice"></eg-alert-dialog>
-
-<!-- global toast alerts -->
-<eg-toast></eg-toast>
-
-<!-- global print handler component -->
-<eg-print></eg-print>
-
-<!-- context menu DOM insertion point -->
-<eg-context-menu-container></eg-context-menu-container>
-
-<eg-string i18n-text key="scko.unknown" text="Unknown Error Occurred"></eg-string>
-<eg-string i18n-text key="scko.checkout.success" text="Checkout Succeeded"></eg-string>
-<eg-string i18n-text key="scko.renew.success" text="Renewal Succeeded"></eg-string>
-<eg-string i18n-text key="scko.item.not_found" 
-  text="Item was not found in the system. Try re-scanning the item."></eg-string>
-<eg-string i18n-text key="scko.checkout.already_out" 
-  text="Item is checked out to another patron"></eg-string>
-
-<ng-template #maxRenew>No more renewals allowed for item {{barcode}}</ng-template>
-<eg-string i18n-text key="scko.error.max_renewals" [template]="maxRenew"></eg-string>
-
-<eg-string key="scko.error.patron_fines"
-  text="This account has too many fines to checkout."></eg-string>
-
-<ng-template #itemNotCataloged>
-  Item {{barcode}} was not found in the system.  Try re-scanning the item.
-</ng-template>
-<eg-string key="scko.error.item_not_cataloged" [template]="itemNotCataloged"> </eg-string>
-
-
-<ng-template #copyCircNotAllowed>
-  Item {{barcode}} is not allowed to circulate</ng-template>
-<eg-string key="scko.error.copy_circ_not_allowed" [template]="copyCircNotAllowed">
-</eg-string>
-
-<eg-string i18n-text key="scko.error.actor_usr_barred" 
-  text="The patron is barred"></eg-string>
-<eg-string i18n-text key="scko.error.asset_copy_circulate" 
-  text="The item does not circulate"></eg-string>
-<eg-string i18n-text key="scko.error.asset_copy_location_circulate" 
-  text="Items from this shelving location do not circulate"></eg-string>
-<eg-string i18n-text key="scko.error.asset_copy_status" 
-  text="The item cannot circulate at this time"></eg-string>
-<eg-string i18n-text key="scko.error.circ_holds_target_skip_me" 
-text="The item's circulation library does not fulfill holds"></eg-string>
-<eg-string i18n-text key="scko.error.config_circ_matrix_circ_mod_test" 
-  text="The patron has too many items of this type checked out"></eg-string>
-<eg-string i18n-text key="scko.error.config_circ_matrix_test_available_copy_hold_ratio" 
-  text="The available item-to-hold ratio is too low"></eg-string>
-<eg-string i18n-text key="scko.error.config_circ_matrix_test_circulate"
-  text="Circulation rules reject this item as non-circulatable"></eg-string>
-<eg-string i18n-text key="scko.error.config_circ_matrix_test_total_copy_hold_ratio"
-  text="The total item-to-hold ratio is too low"></eg-string>
-<eg-string i18n-text key="scko.error.config_hold_matrix_test_holdable"
-  text="Hold rules reject this item as unholdable"></eg-string>
-<eg-string i18n-text key="scko.error.config_hold_matrix_test_max_holds"
-  text="The patron has reached the maximum number of holds"></eg-string>
-<eg-string i18n-text key="scko.error.item.holdable"
-  text="The item is not holdable"></eg-string>
-<eg-string i18n-text key="scko.error.location.holdable"
-  text="The item's location is not holdable"></eg-string>
-<eg-string i18n-text key="scko.error.status.holdable"
-  text="The item is not in a holdable status"></eg-string>
-<eg-string i18n-text key="scko.error.config_rule_age_hold_protect_prox"
-  text="The item is too new to transit this far"></eg-string>
-<eg-string i18n-text key="scko.error.no_item"
-  text="The system could not find this item"></eg-string>
-<eg-string i18n-text key="scko.error.no_ultimate_items"
-  text="The system could not find any items to match this hold request"></eg-string>
-<eg-string i18n-text key="scko.error.no_matchpoint"
-  text="System rules do not define how to handle this item"></eg-string>
-<eg-string i18n-text key="scko.error.no_user"
-  text="The system could not find this patron"></eg-string>
-<eg-string i18n-text key="scko.error.transit_range"
-  text="The item cannot transit this far"></eg-string>
-<eg-string i18n-text key="scko.error.patron_exceeds_checkout_count"
-  text="Maximum checkouts reached on this account"></eg-string>
-
diff --git a/Open-ILS/src/eg2/src/app/scko/scko.component.ts b/Open-ILS/src/eg2/src/app/scko/scko.component.ts
deleted file mode 100644 (file)
index 1cea153..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-import {Component, OnInit, AfterViewInit, ViewChild, ViewEncapsulation} from '@angular/core';
-import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
-import {AuthService} from '@eg/core/auth.service';
-import {NetService} from '@eg/core/net.service';
-import {SckoService} from './scko.service';
-import {ServerStoreService} from '@eg/core/server-store.service';
-import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
-import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
-
-@Component({
-  templateUrl: 'scko.component.html',
-  styleUrls: ['scko.component.css'],
-  encapsulation: ViewEncapsulation.None
-})
-
-export class SckoComponent implements OnInit, AfterViewInit {
-
-    @ViewChild('logoutDialog') logoutDialog: ConfirmDialogComponent;
-    @ViewChild('alertDialog') alertDialog: ConfirmDialogComponent;
-
-    constructor(
-        private router: Router,
-        private route: ActivatedRoute,
-        private net: NetService,
-        private auth: AuthService,
-        public  scko: SckoService
-    ) {}
-
-    ngOnInit() {
-        this.net.authExpired$.subscribe(how => {
-            console.debug('SCKO auth expired with info', how);
-            this.scko.logoutStaff();
-        });
-
-        this.scko.load();
-    }
-
-    ngAfterViewInit() {
-        this.scko.logoutDialog = this.logoutDialog;
-        this.scko.alertDialog = this.alertDialog;
-    }
-}
-
diff --git a/Open-ILS/src/eg2/src/app/scko/scko.module.ts b/Open-ILS/src/eg2/src/app/scko/scko.module.ts
deleted file mode 100644 (file)
index da4f188..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-import {NgModule} from '@angular/core';
-import {EgCommonModule} from '@eg/common.module';
-import {CommonWidgetsModule} from '@eg/share/common-widgets.module';
-import {AudioService} from '@eg/share/util/audio.service';
-import {TitleComponent} from '@eg/share/title/title.component';
-import {PatronModule} from '@eg/staff/share/patron/patron.module';
-
-import {SckoComponent} from './scko.component';
-import {SckoRoutingModule} from './routing.module';
-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';
-import {SckoHoldsComponent} from './holds.component';
-import {SckoFinesComponent} from './fines.component';
-
-@NgModule({
-  declarations: [
-    SckoComponent,
-    SckoBannerComponent,
-    SckoSummaryComponent,
-    SckoCheckoutComponent,
-    SckoItemsComponent,
-    SckoHoldsComponent,
-    SckoFinesComponent
-  ],
-  imports: [
-    EgCommonModule,
-    CommonWidgetsModule,
-    PatronModule,
-    SckoRoutingModule
-  ],
-  providers: [
-    SckoService,
-    AudioService
-  ]
-})
-
-export class SckoModule {}
-
diff --git a/Open-ILS/src/eg2/src/app/scko/scko.service.ts b/Open-ILS/src/eg2/src/app/scko/scko.service.ts
deleted file mode 100644 (file)
index 501e094..0000000
+++ /dev/null
@@ -1,693 +0,0 @@
-import {Injectable, EventEmitter} from '@angular/core';
-import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
-import {Observable} from 'rxjs';
-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 {PatronService, PatronSummary, PatronStats} from '@eg/staff/share/patron/patron.service';
-import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
-import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
-import {PrintService} from '@eg/share/print/print.service';
-import {AudioService} from '@eg/share/util/audio.service';
-import {StringService} from '@eg/share/string/string.service';
-import {PcrudService} from '@eg/core/pcrud.service';
-
-interface CheckoutContext {
-    barcode: string; // item
-    result: any;
-    firstEvent: EgEvent;
-    payload: any;
-    override: boolean;
-    redo: boolean;
-    renew: boolean;
-    displayText: string; // string key
-    alertSound: string;
-    shouldPopup: boolean;
-    previousCirc?: IdlObject;
-    renewalFailure?: boolean;
-}
-
-interface SessionCheckout {
-    circ: IdlObject;
-    ctx: CheckoutContext;
-}
-
-const CIRC_FLESH_DEPTH = 4;
-const CIRC_FLESH_FIELDS = {
-  circ: ['target_copy'],
-  acp:  ['call_number'],
-  acn:  ['record'],
-  bre:  ['flat_display_entries']
-};
-
-@Injectable({providedIn: 'root'})
-export class SckoService {
-
-    // Currently active patron account object.
-    patronSummary: PatronSummary;
-    statusDisplayText = '';
-    statusDisplaySuccess: boolean;
-
-    barcodeRegex: RegExp;
-    patronPasswordRequired = false;
-    patronIdleTimeout: number;
-    patronTimeoutId: number;
-    logoutWarningTimeout = 20;
-    logoutWarningTimerId: number;
-
-    alertAudio = false;
-    alertPopup = false;
-    orgSettings: any;
-    overrideCheckoutEvents: string[] = [];
-    blockStatuses: number[] = [];
-
-    sessionCheckouts: SessionCheckout[] = [];
-
-    // We get this from the main scko component.
-    logoutDialog: ConfirmDialogComponent;
-    alertDialog: AlertDialogComponent;
-    focusBarcode: EventEmitter<void> = new EventEmitter<void>();
-    patronLoaded: EventEmitter<void> = new EventEmitter<void>();
-
-    constructor(
-        private router: Router,
-        private route: ActivatedRoute,
-        private org: OrgService,
-        private net: NetService,
-        private evt: EventService,
-        public auth: AuthService,
-        private pcrud: PcrudService,
-        private printer: PrintService,
-        private audio: AudioService,
-        private strings: StringService,
-        private patrons: PatronService,
-    ) {}
-
-    logoutStaff() {
-        this.resetPatron();
-        this.auth.logout();
-        this.router.navigate(['/scko']);
-    }
-
-    resetPatron() {
-        this.statusDisplayText = '';
-        this.patronSummary = null;
-        this.sessionCheckouts = [];
-    }
-
-    load(): Promise<any> {
-        this.auth.authDomain = 'eg.scko';
-
-        return this.auth.testAuthToken()
-
-        .then(_ => {
-
-            // 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',
-                'circ.selfcheck.patron_password_required',
-                'circ.checkout_auto_renew_age',
-                'circ.selfcheck.workstation_required',
-                'circ.selfcheck.alert.popup',
-                'circ.selfcheck.alert.sound',
-                'credit.payments.allow',
-                'circ.selfcheck.block_checkout_on_copy_status'
-            ]);
-
-        }).then(sets => {
-            this.orgSettings = sets;
-
-            const regPattern = sets['opac.barcode_regex'] || /^\d/;
-            this.barcodeRegex = new RegExp(regPattern);
-            this.patronPasswordRequired =
-                sets['circ.selfcheck.patron_password_required'];
-
-            this.alertAudio = sets['circ.selfcheck.alert.sound'];
-            this.alertPopup = sets['circ.selfcheck.alert.popup'];
-
-            this.overrideCheckoutEvents =
-                sets['circ.selfcheck.auto_override_checkout_events'] || [];
-
-            this.blockStatuses =
-                sets['circ.selfcheck.block_checkout_on_copy_status'] ?
-                sets['circ.selfcheck.block_checkout_on_copy_status'].map(s => Number(s)) :
-                [];
-
-            this.patronIdleTimeout =
-                Number(sets['circ.selfcheck.patron_login_timeout'] || 160);
-
-            // Compensate for the warning dialog
-            this.patronIdleTimeout -= this.logoutWarningTimeout;
-
-            // 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);
-            }
-        });
-    }
-
-    getFleshedCircs(circIds: number[]): Observable<IdlObject> {
-        return this.pcrud.search('circ', {id: circIds}, {
-            flesh: CIRC_FLESH_DEPTH,
-            flesh_fields: CIRC_FLESH_FIELDS,
-            order_by : {circ : 'due_date'},
-            select: {bre : ['id']}
-        });
-    }
-
-    getFleshedCirc(circId: number): Promise<IdlObject> {
-        return this.getFleshedCircs([circId]).toPromise();
-    }
-
-    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)
-        .then(_ => this.resetPatronTimeout())
-        .then(_ => this.patronLoaded.emit());
-    }
-
-    resetPatronTimeout() {
-        console.debug('Resetting patron timeout=' + this.patronIdleTimeout);
-        if (this.patronTimeoutId) {
-            clearTimeout(this.patronTimeoutId);
-        }
-        this.startPatronTimer();
-    }
-
-    startPatronTimer() {
-        this.patronTimeoutId = setTimeout(
-            () => this.showPatronLogoutWarning(),
-            this.patronIdleTimeout * 1000
-        );
-    }
-
-    showPatronLogoutWarning() {
-        console.debug('Session timing out.  Show warning dialog');
-
-        this.logoutDialog.open().subscribe(remain => {
-            if (remain) {
-                clearTimeout(this.logoutWarningTimerId);
-                this.logoutWarningTimerId = null;
-                this.resetPatronTimeout();
-            } else {
-                this.resetPatron();
-                this.router.navigate(['/scko']);
-            }
-        });
-
-        // Force the session to end if no action is taken on the
-        // logout warning dialog.
-        this.logoutWarningTimerId = setTimeout(
-            () => {
-                console.debug('Clearing patron on warning dialog timeout');
-                this.resetPatron();
-                this.router.navigate(['/scko']);
-            },
-            this.logoutWarningTimeout * 1000
-        );
-    }
-
-    sessionTotalCheckouts(): number {
-        return this.sessionCheckouts.length;
-    }
-
-    accountTotalCheckouts(): number {
-        // stats.checkouts.total_out includes claims returned
-
-        return this.sessionTotalCheckouts() +
-            this.patronSummary.stats.checkouts.out +
-            this.patronSummary.stats.checkouts.overdue +
-            this.patronSummary.stats.checkouts.long_overdue;
-    }
-
-
-    checkout(barcode: string, override?: boolean): Promise<any> {
-        this.resetPatronTimeout();
-
-        barcode = (barcode || '').trim();
-        if (!barcode) { return Promise.resolve(); }
-
-        let method = 'open-ils.circ.checkout.full';
-        if (override) { method += '.override'; }
-
-        return this.net.request(
-            'open-ils.circ', method, this.auth.token(), {
-            patron_id: this.patronSummary.id,
-            copy_barcode: barcode
-        }).toPromise()
-
-        .then(result => {
-
-            console.debug('CO returned', result);
-
-            return this.handleCheckoutResult(result, barcode, 'checkout');
-
-        }).then(ctx => {
-            console.debug('handleCheckoutResult returned', ctx);
-
-            if (ctx.override) {
-                return this.checkout(barcode, true);
-            } else if (ctx.redo) {
-                return this.checkout(barcode);
-            } else if (ctx.renew) {
-                return this.renew(barcode);
-            }
-
-            return ctx;
-
-        // Checkout actions always takes us back to the main page
-        // so we can see our items out in progress.
-        })
-        .then(ctx => this.notifyPatron(ctx))
-        .finally(() => this.router.navigate(['/scko']));
-    }
-
-    renew(barcode: string, override?: boolean): Promise<any> {
-
-        let method = 'open-ils.circ.renew';
-        if (override) { method += '.override'; }
-
-        return this.net.request(
-            'open-ils.circ', method, this.auth.token(), {
-            patron_id: this.patronSummary.id,
-            copy_barcode: barcode
-        }).toPromise()
-
-        .then(result => {
-            console.debug('Renew returned', result);
-
-            return this.handleCheckoutResult(result, barcode, 'renew');
-
-        }).then(ctx => {
-            console.debug('handleCheckoutResult returned', ctx);
-
-            if (ctx.override) {
-                return this.renew(barcode, true);
-            }
-        });
-    }
-
-    notifyPatron(ctx: CheckoutContext) {
-        console.debug('notifyPatron(): ', ctx);
-
-        this.statusDisplayText = '';
-
-        this.statusDisplaySuccess = !ctx.shouldPopup;
-
-        this.focusBarcode.emit();
-
-        // TODO on success tell the summary to update its numbers.
-
-        if (this.alertAudio && ctx.alertSound) {
-            this.audio.play(ctx.alertSound);
-        }
-
-        if (!ctx.displayText) { return; }
-
-        this.strings.interpolate(ctx.displayText, {barcode: ctx.barcode})
-        .then(str => {
-            this.statusDisplayText = str;
-
-            if (this.alertPopup && ctx.shouldPopup && str) {
-                this.alertDialog.dialogBody = str;
-                this.alertDialog.open().toPromise();
-            }
-        });
-    }
-
-    handleCheckoutResult(
-        result: any, barcode: string, action: string): Promise<CheckoutContext> {
-
-        if (Array.isArray(result)) {
-            result = result[0];
-        }
-
-        const evt: any = this.evt.parse(result) || {};
-        const payload = evt.payload || {};
-
-        if (evt.textcode === 'NO_SESSION') {
-            this.logoutStaff();
-            return;
-        }
-
-        const ctx: CheckoutContext = {
-            result: result,
-            firstEvent: evt,
-            payload: payload,
-            barcode: barcode,
-            displayText: 'scko.unknown',
-            alertSound: '',
-            shouldPopup: false,
-            redo: false,
-            override: false,
-            renew: false
-        };
-
-        if (evt.textcode === 'SUCCESS') {
-            ctx.displayText = `scko.${action}.success`;
-            ctx.alertSound = `success.scko.${action}`;
-
-            return this.getFleshedCirc(payload.circ.id()).then(
-                circ => {
-                    this.sessionCheckouts.push({circ: circ, ctx: ctx});
-                    return ctx;
-                }
-            );
-        }
-
-        if (evt.textcode === 'OPEN_CIRCULATION_EXISTS' && action === 'checkout') {
-            return this.handleOpenCirc(ctx);
-        }
-
-        return this.handleEvents(ctx);
-    }
-
-    handleOpenCirc(ctx: CheckoutContext): Promise<any> {
-
-        if (ctx.payload.old_circ) {
-            const age = this.orgSettings['circ.checkout_auto_renew_age'];
-
-            if (!age || (age && ctx.payload.auto_renew)) {
-                ctx.renew = true;
-
-                // Flesh the previous circ so we can show the title,
-                // etc. in the receipt.
-                return this.getFleshedCirc(ctx.payload.old_circ.id())
-                .then(oldCirc => {
-                    ctx.previousCirc = oldCirc;
-                    return ctx;
-                });
-            }
-        }
-
-        // LOST items can be checked in and made usable if configured.
-        if (ctx.payload.copy
-            && Number(ctx.payload.copy.status()) === /* LOST */ 3
-            && this.overrideCheckoutEvents.length
-            && this.overrideCheckoutEvents.includes('COPY_STATUS_LOST')) {
-
-            return this.checkin(ctx.barcode).then(ok => {
-                if (ok) {
-                    ctx.redo = true;
-                } else {
-                    ctx.shouldPopup = true;
-                    ctx.alertSound = 'error.scko.checkout';
-                    ctx.displayText = 'scko.checkout.already_out';
-                }
-
-                return ctx;
-            });
-        }
-
-        ctx.shouldPopup = true;
-        ctx.alertSound = 'error.scko.checkout';
-        ctx.displayText = 'scko.checkout.already_out';
-
-        return Promise.resolve(ctx);
-    }
-
-    handleEvents(ctx: CheckoutContext): Promise<CheckoutContext> {
-        let override = true;
-        let abortTransit = false;
-        let lastErrorText = '';
-
-        [].concat(ctx.result).some(res => {
-
-            if (!this.overrideCheckoutEvents.includes(res.textcode)) {
-                console.debug('We are not configured to override', res.textcode);
-                lastErrorText = this.getErrorDisplyText(this.evt.parse(res));
-                return override = false;
-            }
-
-            if (this.blockStatuses.length > 0) {
-                let stat = res.payload.status();
-                if (typeof stat === 'object') { stat = stat.id(); }
-
-                if (this.blockStatuses.includes(Number(stat))) {
-                    return override = false;
-                }
-            }
-
-            if (res.textcode === 'COPY_IN_TRANSIT') {
-                abortTransit = true;
-            }
-
-            return true;
-        });
-
-        if (!override) {
-            ctx.shouldPopup = true;
-            ctx.alertSound = 'error.scko.checkout';
-            ctx.renewalFailure = true;
-            ctx.displayText = lastErrorText;
-            return Promise.resolve(ctx);
-        }
-
-        if (!abortTransit) {
-            ctx.override = true;
-            return Promise.resolve(ctx);
-        }
-
-        return this.checkin(ctx.barcode, true).then(ok => {
-            if (ok) {
-                ctx.redo = true;
-            } else {
-                ctx.shouldPopup = true;
-                ctx.alertSound = 'error.scko.checkout';
-            }
-            return ctx;
-        });
-    }
-
-    getErrorDisplyText(evt: EgEvent): string {
-
-        switch (evt.textcode) {
-            case 'PATRON_EXCEEDS_CHECKOUT_COUNT':
-                return 'scko.error.patron_exceeds_checkout_count';
-            case 'MAX_RENEWALS_REACHED':
-                return 'scko.error.max_renewals';
-            case 'ITEM_NOT_CATALOGED':
-                return 'scko.error.item_not_cataloged';
-            case 'COPY_CIRC_NOT_ALLOWED':
-                return 'scko.error.copy_circ_not_allowed';
-            case 'OPEN_CIRCULATION_EXISTS':
-                return 'scko.error.already_out';
-            case 'PATRON_EXCEEDS_FINES':
-                return 'scko.error.patron_fines';
-            default:
-                if (evt.payload && evt.payload.fail_part) {
-                    return 'scko.error.' +
-                        evt.payload.fail_part.replace(/\./g, '_');
-                }
-        }
-
-        return 'scko.error.unknown';
-    }
-
-    checkin(barcode: string, abortTransit?: boolean): Promise<boolean> {
-
-        let promise = Promise.resolve(true);
-
-        if (abortTransit) {
-
-            promise = this.net.request(
-                'open-ils.circ',
-                'open-ils.circ.transit.abort',
-                this.auth.token(), {barcode: barcode}).toPromise()
-
-            .then(resp => {
-
-                console.debug('Transit abort returned', resp);
-                return Number(resp) === 1;
-            });
-        }
-
-        promise = promise.then(ok => {
-            if (!ok) { return false; }
-
-            return this.net.request(
-                'open-ils.circ',
-                'open-ils.circ.checkin.override',
-                this.auth.token(), {
-                    patron_id : this.patronSummary.id,
-                    copy_barcode : barcode,
-                    noop : true
-                }
-
-            ).toPromise().then(resp => {
-
-                // If any response events are non-success, report the
-                // checkin as a failure.
-                let success = true;
-                [].concat(resp).forEach(evt => {
-                    console.debug('Checkin returned', resp);
-
-                    const code = evt.textcode;
-                    if (code !== 'SUCCESS' && code !== 'NO_CHANGE') {
-                        success = false;
-                    }
-                });
-
-                return success;
-
-            });
-        });
-
-        return promise;
-    }
-
-    logoutPatron(receiptType: string): Promise<any> {
-
-        let promise;
-
-        switch (receiptType) {
-            case 'email':
-                promise = this.emailReceipt();
-                break;
-            case 'print':
-                promise = this.printReceipt();
-                break;
-            default:
-                promise = Promise.resolve();
-        }
-
-        return promise.then(_ => {
-            this.resetPatron();
-            this.router.navigate(['/scko']);
-        });
-    }
-
-    emailReceipt(): Promise<any> {
-
-        const circIds = this.sessionCheckouts
-            .filter(c => Boolean(c.circ)).map(c => c.circ.id());
-
-        return this.net.request(
-            'open-ils.circ',
-            'open-ils.circ.checkout.batch_notify.session.atomic',
-            this.auth.token(), this.patronSummary.id, circIds
-        ).toPromise();
-    }
-
-    printReceipt(): Promise<any> {
-
-        return new Promise((resolve, reject) => {
-
-            const sub = this.printer.printJobQueued$.subscribe(_ => {
-                sub.unsubscribe();
-                // Give the print operation just a bit more time after
-                // the data is passed to the printer just to be safe.
-                setTimeout(() => resolve(null), 1000);
-            });
-
-            const data = this.sessionCheckouts.map(c => {
-                const circ = c.circ || c.ctx.previousCirc;
-                return {
-                    checkout: c,
-                    barcode: c.ctx.barcode,
-                    circ: circ,
-                    copy: circ ? circ.target_copy() : null,
-                    title: this.getCircTitle(circ),
-                    author: this.getCircAuthor(circ)
-                };
-            });
-
-            this.printer.print({
-                templateName: 'scko_checkouts',
-                contextData: {
-                    checkouts: data,
-                    user: this.patronSummary.patron
-                },
-                printContext: 'default'
-            });
-        });
-    }
-
-    copyIsPrecat(copy: IdlObject): boolean {
-        return Number(copy.id()) === -1;
-    }
-
-    circDisplayValue(circ: IdlObject, field: string): string {
-        if (!circ) { return ''; }
-
-        const entry =
-            circ.target_copy().call_number().record().flat_display_entries()
-            .filter(e => e.name() === field)[0];
-
-        return entry ? entry.value() : '';
-    }
-
-    getCircTitle(circ: IdlObject): string {
-        if (!circ) { return ''; }
-        const copy = circ.target_copy();
-        if (this.copyIsPrecat(copy)) { return copy.dummy_title(); }
-        return this.circDisplayValue(circ, 'title');
-    }
-
-    getCircAuthor(circ: IdlObject): string {
-        if (!circ) { return ''; }
-        const copy = circ.target_copy();
-        if (this.copyIsPrecat(copy)) { return copy.dummy_author(); }
-        return this.circDisplayValue(circ, 'author');
-    }
-
-}
-
-
-
diff --git a/Open-ILS/src/eg2/src/app/scko/summary.component.html b/Open-ILS/src/eg2/src/app/scko/summary.component.html
deleted file mode 100644 (file)
index f7329a0..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-
-<div id="scko-circ-info-div">
-
-  <div class="pl-2">
-    <span>
-      <a routerLink="/scko" class="mr-2">
-        <button type="button" class="scko-button">Home</button>
-      </a>
-    </span>
-    <span>
-      <a (click)="scko.logoutPatron(receiptType)">
-        <button type="button" class="scko-button">Logout</button>
-      </a>
-    </span>
-  </div>
-
-  <div class="pl-2 mt-3">
-    <span class="mr-2" i18n>Receipt: </span>
-
-    <div *ngIf="showEmailOption" class="form-check form-check-inline">
-      <input class="form-check-input" name="receipt-type" type="radio" 
-        [(ngModel)]="receiptType" id="receipt-email" value="email">
-      <label class="form-check-label" for="receipt-email" i18n>Email</label>
-    </div>
-
-    <div class="form-check form-check-inline">
-      <input class="form-check-input" name="receipt-type" type="radio" 
-        [(ngModel)]="receiptType" id="receipt-print" value="print">
-      <label class="form-check-label" for="receipt-print" i18n>Print</label>
-    </div>
-
-    <div class="form-check form-check-inline">
-      <input class="form-check-input" name="receipt-type" type="radio" 
-        [(ngModel)]="receiptType" id="receipt-none" value="none">
-      <label class="form-check-label" for="receipt-none" i18n>None</label>
-    </div>
-  </div>
-
-  <fieldset>
-    <legend i18n>Items Checked Out</legend>
-    <div>
-      <span i18n>Total items this session: </span>
-      <span class="font-weight-bold">{{scko.sessionTotalCheckouts()}}</span>
-    </div>
-    <div class="mt-2">
-      <span i18n>Total items on account: </span>
-      <span class="font-weight-bold">{{scko.accountTotalCheckouts()}}</span>
-    </div>
-    <div class="mt-2">
-      <a routerLink="/scko/items">
-        <button type="button" class="scko-button" i18n>View Items Out</button>
-      </a>
-    </div>
-  </fieldset>
-  <fieldset>
-    <legend i18n>Holds</legend>
-    <div i18n>
-      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 
-      <span class="font-weight-bold">{{scko.patronSummary.stats.holds.total}}</span>
-      total holds.
-    </div>
-    <div class="mt-2">
-      <a routerLink="/scko/holds" id="oils-selfck-hold-details-link">
-        <button type="button" class="scko-button" i18n>View Holds</button>
-      </a>
-    </div>
-  </fieldset>
-  <fieldset>
-    <legend i18n>Fines</legend>
-    <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 routerLink="/scko/fines" id="oils-selfck-view-fines-link">
-          <button type="button" class="scko-button" i18n>View Details</button>
-        </a>
-      </span>
-    </div>
-  </fieldset>
-</div>
-
-
diff --git a/Open-ILS/src/eg2/src/app/scko/summary.component.ts b/Open-ILS/src/eg2/src/app/scko/summary.component.ts
deleted file mode 100644 (file)
index 454cabe..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-import {Component, OnInit, NgZone, HostListener} from '@angular/core';
-import {Location} from '@angular/common';
-import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
-import {AuthService, AuthWsState} from '@eg/core/auth.service';
-import {NetService} from '@eg/core/net.service';
-import {StoreService} from '@eg/core/store.service';
-import {SckoService} from './scko.service';
-import {OrgService} from '@eg/core/org.service';
-import {EventService, EgEvent} from '@eg/core/event.service';
-
-@Component({
-  selector: 'eg-scko-summary',
-  templateUrl: 'summary.component.html'
-})
-
-export class SckoSummaryComponent implements OnInit {
-
-    showEmailOption = false;
-    receiptType = 'email';
-
-    constructor(
-        public scko: SckoService
-    ) {}
-
-    ngOnInit() {
-        this.scko.patronLoaded.subscribe(() => {
-            if (this.canEmail()) {
-                this.showEmailOption = true;
-                this.receiptType = 'email';
-            } else {
-                this.showEmailOption = false;
-                this.receiptType = 'print';
-            }
-        });
-    }
-
-    canEmail(): boolean {
-        if (!this.scko.patronSummary) { return false; }
-
-        const patron = this.scko.patronSummary.patron;
-
-        const setting = patron.settings().filter(
-            s => s.name() === 'circ.send_email_checkout_receipts')[0];
-
-        return (
-            Boolean(patron.email())
-            && patron.email().match(/.*@.*/) !== null
-            && setting
-            && setting.value() === 'true' // JSON
-        );
-    }
-}
-
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/banner.component.html b/Open-ILS/src/eg2/src/app/staff/scko/banner.component.html
new file mode 100644 (file)
index 0000000..6f72e12
--- /dev/null
@@ -0,0 +1,137 @@
+
+<div id="scko-banner" class="pb-2">
+       <div id="scko-logo-div">
+               <img src="/images/self_eg_logo.png"/>
+       </div>
+  <div class="scko-scan-container mt-3">
+    <ng-container *ngIf="scko.auth.user() && !scko.patronSummary">
+      <div id="scko-scan-input-text" i18n>
+        Please log in with your username or library barcode.
+      </div>
+      <div class="d-flex mt-3 mb-3">
+        <div class="flex-1"></div>
+        <div>
+          <form (ngSubmit)="submitPatronLogin()" #patronLoginForm="ngForm"
+            autocomplete="off" class="form-validated form-inline">
+
+            <label class="sr-only" for="patron-username" i18n>Username</label>
+
+            <input type="text" class="form-control border border-dark shadow-rounded" 
+              autocomplete="off" id="patron-username" required 
+              [(ngModel)]="patronUsername" name="patron-username"
+              placeholder="Username or Barcode" i18n-placeholder>
+
+            <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>
+      </div>
+    </ng-container>
+
+    <ng-container *ngIf="scko.patronSummary">
+      <div id="scko-scan-input-text" i18n>Please enter an item barcode</div>
+      <div class="d-flex mt-3 mb-3">
+        <div class="flex-1"></div>
+        <div>
+          <form (ngSubmit)="submitItemBarcode()" #barcodeForm="ngForm"
+            autocomplete="off" class="form-validated form-inline">
+
+            <label class="sr-only" for="item-barcode" i18n>Item Barcode</label>
+
+            <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>
+
+          </form>
+        </div>
+        <div class="flex-1 d-flex">
+          <div class="flex-1"></div>
+          <div id="scko-welcome-message" class="mr-2 rounded" i18n>Welcome,
+            {{scko.patronSummary.patron.pref_first_given_name() 
+              || scko.patronSummary.patron.first_given_name()}}
+          </div>
+        </div>
+      </div>
+    </ng-container>
+
+  </div>
+</div>
+
+<div *ngIf="!scko.auth.user()" class="container mt-3">
+
+  <div class="col-lg-6 offset-lg-3">
+    <fieldset>
+      <legend class="mb-0" i18n><h1>Self-Checkout Staff Login</h1></legend>
+      <hr class="mt-1"/>
+      <form (ngSubmit)="submitStaffLogin()" #staffLoginForm="ngForm" class="form-validated">
+
+        <div class="form-group row">
+          <label class="col-lg-4 text-right font-weight-bold" 
+            for="staff-username" i18n>Username</label>
+          <input 
+            type="text" 
+            class="form-control col-lg-8"
+            id="staff-username" 
+            name="staff-username"
+            required
+            autocomplete="username"
+            i18n-placeholder
+            placeholder="Staff Username" 
+            [(ngModel)]="staffUsername"/>
+        </div>
+
+        <div class="form-group row">
+          <label class="col-lg-4 text-right font-weight-bold" 
+            for="staff-password" i18n>Password</label>
+          <input 
+            type="password" 
+            class="form-control col-lg-8"
+            id="staff-password" 
+            name="staff-password"
+            required
+            autocomplete="current-password"
+            i18n-placeholder
+            placeholder="Staff Password" 
+            [(ngModel)]="staffPassword"/>
+        </div>
+
+        <div class="form-group row" *ngIf="workstations && workstations.length">
+          <label class="col-lg-4 text-right font-weight-bold" 
+            for="workstation" i18n>Workstation</label>
+          <select 
+            class="form-control col-lg-8" 
+            id="workstation" 
+            name="workstation"
+            required
+            [(ngModel)]="staffWorkstation">
+            <option *ngFor="let ws of workstations" [value]="ws.name">
+              {{ws.name}}
+            </option>
+          </select>
+        </div>
+
+        <div class="row">
+          <div class="col-lg-2 offset-lg-4 pl-0">
+            <button type="submit" class="btn btn-outline-dark" i18n>Sign In</button>
+          </div>
+          <div class="col-lg-3">
+            <a href="/eg/staff/admin/workstation/workstations" i18n>
+              Manage Workstations
+            </a>
+          </div>
+          <div class="col-lg-3">
+            <div *ngIf="loginFailed" class="badge badge-warning p-2" i18n>Login Failed</div>
+          </div>
+        </div>
+      </form>
+    </fieldset>
+  </div>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/banner.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/banner.component.ts
new file mode 100644 (file)
index 0000000..11476d2
--- /dev/null
@@ -0,0 +1,144 @@
+import {Component, OnInit, AfterViewInit, NgZone, HostListener} from '@angular/core';
+import {Location} from '@angular/common';
+import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
+import {AuthService, AuthWsState} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {StoreService} from '@eg/core/store.service';
+import {SckoService} from './scko.service';
+import {OrgService} from '@eg/core/org.service';
+import {EventService, EgEvent} from '@eg/core/event.service';
+import {HatchService} from '@eg/core/hatch.service';
+
+@Component({
+  selector: 'eg-scko-banner',
+  templateUrl: 'banner.component.html'
+})
+
+export class SckoBannerComponent implements OnInit, AfterViewInit {
+
+    workstations: any[];
+    workstationNotFound = false;
+
+    patronUsername: string;
+    patronPassword: string;
+    patronLoginFailed = false;
+
+    staffUsername: string;
+    staffPassword: string;
+    staffWorkstation: string;
+    staffLoginFailed = false;
+
+    itemBarcode: string;
+
+    constructor(
+        private route: ActivatedRoute,
+        private store: StoreService,
+        private net: NetService,
+        private auth: AuthService,
+        private evt: EventService,
+        private ngLocation: Location,
+        private org: OrgService,
+        private hatch: HatchService,
+        public scko: SckoService
+    ) {}
+
+    ngOnInit() {
+
+        this.hatch.connect();
+
+        this.store.getWorkstations()
+        .then(wsList => {
+            this.workstations = wsList;
+            return this.store.getDefaultWorkstation();
+        }).then(def => {
+            this.staffWorkstation = def;
+            this.applyWorkstation();
+        });
+    }
+
+    ngAfterViewInit() {
+        if (this.auth.token()) {
+            this.focusNode('patron-username');
+        } else {
+            this.focusNode('staff-username');
+        }
+
+        this.scko.focusBarcode.subscribe(_ => this.focusNode('item-barcode'));
+    }
+
+    focusNode(id: string) {
+        setTimeout(() => {
+            const node = document.getElementById(id);
+            if (node) { (node as HTMLInputElement).select(); }
+        });
+    }
+
+    applyWorkstation() {
+        const wanted = this.route.snapshot.queryParamMap.get('workstation');
+        if (!wanted) { return; } // use the default
+
+        const exists = this.workstations.filter(w => w.name === wanted)[0];
+        if (exists) {
+            this.staffWorkstation = wanted;
+        } else {
+            console.error(`Unknown workstation requested: ${wanted}`);
+        }
+    }
+
+    submitStaffLogin() {
+
+        this.staffLoginFailed = false;
+
+        const args = {
+            type: 'persistent',
+            username: this.staffUsername,
+            password: this.staffPassword,
+            workstation: this.staffWorkstation
+        };
+
+        this.staffLoginFailed = false;
+        this.workstationNotFound = false;
+
+        this.auth.login(args).then(
+            ok => {
+
+                if (this.auth.workstationState === AuthWsState.NOT_FOUND_SERVER) {
+                    this.staffLoginFailed = true;
+                    this.workstationNotFound = true;
+
+                } else {
+
+                    // Initial login clears cached org unit setting values
+                    // and user/workstation setting values
+                    this.org.clearCachedSettings().then(_ => {
+
+                        // Force reload of the app after a successful login.
+                        window.location.href =
+                            this.ngLocation.prepareExternalUrl('/staff/scko');
+
+                    });
+                }
+            },
+            notOk => {
+                this.staffLoginFailed = true;
+            }
+        );
+    }
+
+    submitPatronLogin() {
+        this.patronLoginFailed = false;
+        this.scko.loadPatron(this.patronUsername, this.patronPassword).finally(() => {
+            if (this.scko.patronSummary === null) {
+                this.patronLoginFailed = true;
+            } else {
+                this.focusNode('item-barcode');
+            }
+        });
+    }
+
+    submitItemBarcode() {
+        this.scko.resetPatronTimeout();
+        this.scko.checkout(this.itemBarcode);
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/checkout.component.html b/Open-ILS/src/eg2/src/app/staff/scko/checkout.component.html
new file mode 100644 (file)
index 0000000..51f549f
--- /dev/null
@@ -0,0 +1,35 @@
+<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 co of scko.sessionCheckouts">
+        <td>
+          <ng-container *ngIf="co.circ">
+            <img src="/opac/extras/ac/jacket/small/r/{{co.circ.target_copy().call_number().record().id()}}"/>
+          </ng-container>
+        </td>
+        <td><span *ngIf="co.circ">{{co.circ.target_copy().barcode()}}</span></td>
+        <td>{{scko.getCircTitle(co.circ)}}</td>
+        <td>{{scko.getCircAuthor(co.circ)}}</td>
+        <td><span *ngIf="co.circ">{{co.circ | egDueDate}}</span></td>
+        <td><span *ngIf="co.circ">{{co.circ.renewal_remaining()}}</span></td>
+        <td>
+          <ng-container *ngIf="co.circ">
+            <span *ngIf="co.circ.parent_circ()" i18n>Renewal</span>
+            <span *ngIf="!co.circ.parent_circ()" i18n>Checkout</span>
+          </ng-container>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/checkout.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/checkout.component.ts
new file mode 100644 (file)
index 0000000..5afc1f6
--- /dev/null
@@ -0,0 +1,23 @@
+import {Component, OnInit, ViewEncapsulation} from '@angular/core';
+import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
+import {AuthService} from '@eg/core/auth.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {SckoService} from './scko.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+
+@Component({
+  templateUrl: 'checkout.component.html'
+})
+
+export class SckoCheckoutComponent implements OnInit {
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        public  scko: SckoService
+    ) {}
+
+    ngOnInit() {
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/fines.component.html b/Open-ILS/src/eg2/src/app/staff/scko/fines.component.html
new file mode 100644 (file)
index 0000000..97f7464
--- /dev/null
@@ -0,0 +1,35 @@
+<div class="d-flex">
+  <div class="flex-1"></div>
+  <div>
+    <button type="button" class="scko-button" (click)="printList()" i18n>Print List</button>
+  </div>
+</div>
+<div id='oils-selfck-circ-table-div'>
+  <table id='oils-selfck-circ-table' class='oils-selfck-item-table'>
+    <thead>
+      <tr>
+        <td i18n>Type</td>
+        <td i18n>Details</td>
+        <td i18n>Total Billed</td>
+        <td i18n>Total Paid</td>
+        <td i18n>Balance Owed</td>
+      </tr>
+    </thead>
+    <tbody class='oils-selfck-item-table'>
+           <tr *ngFor="let xact of xacts">
+        <td>
+          <ng-container *ngIf="xact.summary().xact_type() == 'circulation'" i18n>
+            Circulation
+          </ng-container>
+          <ng-container *ngIf="xact.summary().xact_type() != 'circulation'" i18n>
+            Miscellaneous
+          </ng-container>
+        </td>
+        <td>{{getDetails(xact)}}</td>
+        <td>{{xact.summary().total_owed() | currency}}</td>
+        <td>{{xact.summary().total_paid() | currency}}</td>
+        <td>{{xact.summary().balance_owed() | currency}}</td>
+      </tr>
+    </tbody>
+  </table>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/fines.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/fines.component.ts
new file mode 100644 (file)
index 0000000..2209614
--- /dev/null
@@ -0,0 +1,110 @@
+import {Component, OnInit, ViewEncapsulation} from '@angular/core';
+import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
+import {switchMap, 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 {PrintService} from '@eg/share/print/print.service';
+
+
+@Component({
+  templateUrl: 'fines.component.html'
+})
+
+export class SckoFinesComponent implements OnInit {
+
+    xacts: IdlObject[] = [];
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private printer: PrintService,
+        public  scko: SckoService
+    ) {}
+
+    ngOnInit() {
+
+        if (!this.scko.patronSummary) {
+            this.router.navigate(['/staff/scko']);
+            return;
+        }
+
+        this.scko.resetPatronTimeout();
+
+        this.pcrud.search('mbts',
+            {   usr: this.scko.patronSummary.id,
+                xact_finish: null,
+                balance_owed: {'<>' : 0}
+            }, {}, {atomic: true}
+        ).pipe(switchMap(sums => {
+
+            return this.pcrud.search('mbt', {id: sums.map(s => s.id())},
+                {   order_by: 'xact_start',
+                    flesh: 5,
+                    flesh_fields: {
+                        mbt: ['summary', 'circulation', 'grocery'],
+                        circ: ['target_copy'],
+                        acp: ['call_number'],
+                        acn: ['record'],
+                        bre: ['flat_display_entries']
+                    },
+                    select: {bre : ['id']}
+                }
+            ).pipe(tap(xact => this.xacts.push(xact)));
+        })).toPromise();
+    }
+
+    displayValue(xact: IdlObject, field: string): string {
+        const entry =
+            xact.circulation().target_copy().call_number().record().flat_display_entries()
+            .filter(e => e.name() === field)[0];
+
+        return entry ? entry.value() : '';
+    }
+
+    getTitle(xact: IdlObject): string {
+        const copy = xact.circulation().target_copy();
+
+        if (Number(copy.call_number().id()) === -1) {
+            return copy.dummy_title();
+        }
+
+        return this.displayValue(xact, 'title');
+    }
+
+    getDetails(xact: IdlObject): string {
+        if (xact.summary().xact_type() === 'circulation') {
+            return this.getTitle(xact);
+        } else {
+            return xact.summary().last_billing_type();
+        }
+    }
+
+    printList() {
+
+        const data = this.xacts.map(x => {
+            return {
+                xact: x, // full object if needed
+                details: this.getDetails(x),
+                total_owed: x.summary().total_owed(),
+                total_paid: x.summary().total_paid(),
+                balance_owed: x.summary().balance_owed(),
+            };
+        });
+
+        this.printer.print({
+            templateName: 'scko_fines',
+            contextData: {
+                xacts: data,
+                user: this.scko.patronSummary.patron
+            },
+            printContext: 'default'
+        });
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/holds.component.html b/Open-ILS/src/eg2/src/app/staff/scko/holds.component.html
new file mode 100644 (file)
index 0000000..09e3b25
--- /dev/null
@@ -0,0 +1,33 @@
+<div class="d-flex">
+  <div class="flex-1"></div>
+  <div>
+    <button type="button" class="scko-button" (click)="printList()" i18n>Print List</button>
+  </div>
+</div>
+<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>Title</td>
+        <td i18n>Author</td>
+        <td class="rounded-right" i18n>Status</td>
+      </tr>
+    </thead>
+    <tbody class='oils-selfck-item-table'>
+           <tr *ngFor="let hold of holds">
+        <td><img src="/opac/extras/ac/jacket/small/r/{{hold.record_id}}"/></td>
+        <td>{{hold.title}}</td>
+        <td>{{hold.author}}</td>
+        <td>
+          <ng-container *ngIf="hold.hold_status == 4" i18n>
+            Ready for Pickup
+          </ng-container>
+          <ng-container *ngIf="hold.hold_status != 4" i18n>
+            #{{hold.relative_queue_position}} in line with {{hold.potentials}} copie(s)
+          </ng-container>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/holds.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/holds.component.ts
new file mode 100644 (file)
index 0000000..eddc802
--- /dev/null
@@ -0,0 +1,78 @@
+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';
+import {PrintService} from '@eg/share/print/print.service';
+
+@Component({
+  templateUrl: 'holds.component.html'
+})
+
+export class SckoHoldsComponent implements OnInit {
+
+    holds: IdlObject[] = [];
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private auth: AuthService,
+        private pcrud: PcrudService,
+        private printer: PrintService,
+        public  scko: SckoService
+    ) {}
+
+    ngOnInit() {
+
+        if (!this.scko.patronSummary) {
+            this.router.navigate(['/staff/scko']);
+            return;
+        }
+
+        this.scko.resetPatronTimeout();
+
+        const orderBy = [
+           {shelf_time: {nulls: 'last'}},
+           {capture_time: {nulls: 'last'}},
+           {request_time: {nulls: 'last'}}
+        ];
+
+        const filters = {
+            usr_id: this.scko.patronSummary.id,
+            fulfillment_time: null
+        };
+
+        let first = true;
+        this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.hold.wide_hash.stream',
+            this.auth.token(), filters, orderBy, 1000, 0, {}
+        ).subscribe(holdData => {
+
+            if (first) { // First response is the hold count.
+                first = false;
+                return;
+            }
+
+            this.holds.push(holdData);
+        });
+    }
+
+    printList() {
+        this.printer.print({
+            templateName: 'scko_holds',
+            contextData: {
+              holds: this.holds,
+              user: this.scko.patronSummary.patron
+            },
+            printContext: 'default'
+        });
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/items.component.html b/Open-ILS/src/eg2/src/app/staff/scko/items.component.html
new file mode 100644 (file)
index 0000000..63e36db
--- /dev/null
@@ -0,0 +1,39 @@
+<div class="d-flex">
+  <div class="flex-1"></div>
+  <div>
+    <button type="button" class="scko-button" (click)="printList()" i18n>Print List</button>
+  </div>
+</div>
+<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>{{scko.getCircTitle(circ)}}</td>
+        <td>{{scko.getCircAuthor(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/staff/scko/items.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/items.component.ts
new file mode 100644 (file)
index 0000000..2926a27
--- /dev/null
@@ -0,0 +1,75 @@
+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';
+import {PrintService} from '@eg/share/print/print.service';
+
+@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,
+        private printer: PrintService,
+        public  scko: SckoService
+    ) {}
+
+    ngOnInit() {
+
+        if (!this.scko.patronSummary) {
+            this.router.navigate(['/staff/scko']);
+            return;
+        }
+
+        this.scko.resetPatronTimeout();
+
+        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.scko.getFleshedCircs(ids).pipe(tap(circ => {
+                this.circs.push(circ);
+            })).toPromise();
+        });
+    }
+
+    printList() {
+
+        const data = this.circs.map(c => {
+            return {
+                circ: c,
+                copy: c.target_copy(),
+                title: this.scko.getCircTitle(c),
+                author: this.scko.getCircAuthor(c)
+            };
+        });
+
+        this.printer.print({
+            templateName: 'scko_items_out',
+            contextData: {
+                checkouts: data,
+                user: this.scko.patronSummary.patron
+            },
+            printContext: 'default'
+        });
+    }
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/scko/routing.module.ts
new file mode 100644 (file)
index 0000000..0881453
--- /dev/null
@@ -0,0 +1,33 @@
+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';
+import {SckoHoldsComponent} from './holds.component';
+import {SckoFinesComponent} from './fines.component';
+
+const routes: Routes = [{
+  path: '',
+  component: SckoComponent,
+  children: [{
+    path: '',
+    component: SckoCheckoutComponent
+  }, {
+    path: 'items',
+    component: SckoItemsComponent
+   }, {
+    path: 'holds',
+    component: SckoHoldsComponent
+   }, {
+    path: 'fines',
+    component: SckoFinesComponent
+ }]
+}];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class SckoRoutingModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/scko.component.css b/Open-ILS/src/eg2/src/app/staff/scko/scko.component.css
new file mode 100644 (file)
index 0000000..0150ee3
--- /dev/null
@@ -0,0 +1,126 @@
+body {                                                                         
+    font-family: Arial, Verdana;                                               
+    font-size: 13px;                                                           
+}                                                                              
+
+A {
+    text-decoration: none;
+}
+                                                                               
+#scko-banner {                                                         
+    background: #00593d; /* Old browsers */                                    
+/* IE9 SVG, needs conditional override of 'filter' to 'none' */                
+    background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwNTkzZCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMwMDdhNTQiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+);
+    background: -moz-linear-gradient(top,  #00593d 0%, #007a54 100%); /* FF3.6+ */
+    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00593d), color-stop(100%,#007a54)); /* Chrome,Safari4+ */
+    background: -webkit-linear-gradient(top,  #00593d 0%,#007a54 100%); /* Chrome10+,Safari5.1+ */
+    background: -o-linear-gradient(top,  #00593d 0%,#007a54 100%); /* Opera 11.10+ */
+    background: -ms-linear-gradient(top,  #00593d 0%,#007a54 100%); /* IE10+ */
+    background: linear-gradient(to bottom,  #00593d 0%,#007a54 100%); /* W3C */
+    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00593d', endColorstr='#007a54',GradientType=0 ); /* IE6-8 */
+    padding-top: 20px;                                                         
+    text-align: center;                                                        
+    font-weight:bold;                                                          
+}  
+
+#scko-scan-input-text {                                                       
+  font-size: 16px;                                                           
+  background: none repeat scroll 0 0 #252525;                                
+  color: white;                                                              
+  padding: 10px;                                                             
+} 
+
+#scko-welcome-message {
+  font-size: 16px;                                                           
+  background: none repeat scroll 0 0 #252525;                                
+  color: white;                                                              
+  padding: 10px;                                                             
+}
+
+#scko-circ-info-div fieldset {
+    margin: 15px 10px 15px 10px;
+    padding: 8px;
+    background: #ffffff; /* Old browsers */
+    /* IE9 SVG, needs conditional override of 'filter' to 'none' */
+    background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9Ijk4JSIgc3RvcC1jb2xvcj0iI2UwZTBlMCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=);
+    background: -moz-linear-gradient(top,  #ffffff 0%, #e0e0e0 98%); /* FF3.6+ */
+    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(98%,#e0e0e0)); /* Chrome,Safari4+ */
+    background: -webkit-linear-gradient(top,  #ffffff 0%,#e0e0e0 98%); /* Chrome10+,Safari5.1+ */
+    background: -o-linear-gradient(top,  #ffffff 0%,#e0e0e0 98%); /* Opera 11.10+ */
+    background: -ms-linear-gradient(top,  #ffffff 0%,#e0e0e0 98%); /* IE10+ */
+    background: linear-gradient(to bottom,  #ffffff 0%,#e0e0e0 98%); /* W3C */
+    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#e0e0e0',GradientType=0 ); /* IE6-8 */
+    border: 1px solid #DDDDDD;
+    color: #333333;
+    -webkit-border-radius: 5px;
+    border-radius: 5px;
+}
+
+#scko-circ-info-div fieldset legend {
+    font-size: 13px;
+    color: #00593d;
+    background-color: white;
+    font-weight: bold;
+}
+
+.scko-button { 
+    color: #FBF9F9;
+    font-weight: bold;
+    letter-spacing: 1px;
+    font-size: 94%;
+    text-shadow: 1px 1px 1px #555555;
+    cursor: pointer !important;
+    border-radius: 12px;
+    border: 1px solid #007a54;
+    background: linear-gradient(#007a54, #00593d);
+    background: -moz-linear-gradient(#007a54, #00593d);
+    background: -o-linear-gradient(#007a54, #00593d);
+    background: -webkit-linear-gradient(#007a54, #00593d);
+    background-color: #00593d;
+    padding: 5px 10px 6px;
+    outline: 0 none;
+    text-decoration: none;
+}
+
+
+.oils-selfck-item-table  {
+    width: 98%;
+    margin-top: 15px;
+}
+
+.oils-selfck-item-table td {
+    text-align: left;
+    padding: 10px;
+}
+
+.oils-selfck-item-table thead {
+    font-weight: bold;
+    color: #00593D;
+}
+
+.oils-selfck-item-table tbody tr {
+    border-bottom: 1px solid #888;
+}
+
+.scko-page {
+    margin-top: 20px;
+    background: #ffffff; /* Old browsers */
+    /* IE9 SVG, needs conditional override of 'filter' to 'none' */
+    background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9Ijk4JSIgc3RvcC1jb2xvcj0iI2UwZTBlMCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=);
+    background: -moz-linear-gradient(top,  #ffffff 0%, #e0e0e0 98%); /* FF3.6+ */
+    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(98%,#e0e0e0)); /* Chrome,Safari4+ */
+    background: -webkit-linear-gradient(top,  #ffffff 0%,#e0e0e0 98%); /* Chrome10+,Safari5.1+ */
+    background: -o-linear-gradient(top,  #ffffff 0%,#e0e0e0 98%); /* Opera 11.10+ */
+    background: -ms-linear-gradient(top,  #ffffff 0%,#e0e0e0 98%); /* IE10+ */
+    background: linear-gradient(to bottom,  #ffffff 0%,#e0e0e0 98%); /* W3C */
+    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#e0e0e0',GradientType=0 ); /* IE6-8 */
+    border: 1px solid #DDDDDD;
+    color: #333333;
+    -webkit-border-radius: 5px;
+    border-radius: 5px;
+    padding: 3px;
+}
+
+.scko-status-row {
+    font-size: 20px;
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/scko.component.html b/Open-ILS/src/eg2/src/app/staff/scko/scko.component.html
new file mode 100644 (file)
index 0000000..1ae367d
--- /dev/null
@@ -0,0 +1,108 @@
+
+<eg-scko-banner></eg-scko-banner>
+
+<div class="d-flex scko-status-row mt-2">
+  <div class="flex-1"></div>
+  <div [ngClass]="{
+    'text-success': scko.statusDisplaySuccess, 
+    'text-danger': !scko.statusDisplaySuccess}">{{scko.statusDisplayText}}</div>
+  <div class="flex-1"></div>
+</div>
+
+<div *ngIf="scko.auth.token() && scko.patronSummary" class="row mr-0 mt-5">
+  <div class="col-lg-9">
+    <div class="ml-2 scko-page">
+      <router-outlet></router-outlet>
+    </div>
+  </div>
+  <div class="col-lg-3"><eg-scko-summary></eg-scko-summary></div>
+</div>
+
+<eg-confirm-dialog #logoutDialog 
+  i18n-dialogTitle i18n-dialogBody
+  i18n-confirmButtonText i18n-cancelButtonText
+  dialogTitle="Logout Notice"
+  dialogBody="Your login session will timeout due to inactivity"
+  confirmButtonText="Continue Session"
+  cancelButtonText="Logout">
+</eg-confirm-dialog>
+
+<eg-alert-dialog #alertDialog i18n-dialogTitle="Notice"></eg-alert-dialog>
+
+<!-- global toast alerts -->
+<eg-toast></eg-toast>
+
+<!-- global print handler component -->
+<eg-print></eg-print>
+
+<!-- context menu DOM insertion point -->
+<eg-context-menu-container></eg-context-menu-container>
+
+<eg-string i18n-text key="scko.unknown" text="Unknown Error Occurred"></eg-string>
+<eg-string i18n-text key="scko.checkout.success" text="Checkout Succeeded"></eg-string>
+<eg-string i18n-text key="scko.renew.success" text="Renewal Succeeded"></eg-string>
+<eg-string i18n-text key="scko.item.not_found" 
+  text="Item was not found in the system. Try re-scanning the item."></eg-string>
+<eg-string i18n-text key="scko.checkout.already_out" 
+  text="Item is checked out to another patron"></eg-string>
+
+<ng-template #maxRenew>No more renewals allowed for item {{barcode}}</ng-template>
+<eg-string i18n-text key="scko.error.max_renewals" [template]="maxRenew"></eg-string>
+
+<eg-string key="scko.error.patron_fines"
+  text="This account has too many fines to checkout."></eg-string>
+
+<ng-template #itemNotCataloged>
+  Item {{barcode}} was not found in the system.  Try re-scanning the item.
+</ng-template>
+<eg-string key="scko.error.item_not_cataloged" [template]="itemNotCataloged"> </eg-string>
+
+
+<ng-template #copyCircNotAllowed>
+  Item {{barcode}} is not allowed to circulate</ng-template>
+<eg-string key="scko.error.copy_circ_not_allowed" [template]="copyCircNotAllowed">
+</eg-string>
+
+<eg-string i18n-text key="scko.error.actor_usr_barred" 
+  text="The patron is barred"></eg-string>
+<eg-string i18n-text key="scko.error.asset_copy_circulate" 
+  text="The item does not circulate"></eg-string>
+<eg-string i18n-text key="scko.error.asset_copy_location_circulate" 
+  text="Items from this shelving location do not circulate"></eg-string>
+<eg-string i18n-text key="scko.error.asset_copy_status" 
+  text="The item cannot circulate at this time"></eg-string>
+<eg-string i18n-text key="scko.error.circ_holds_target_skip_me" 
+text="The item's circulation library does not fulfill holds"></eg-string>
+<eg-string i18n-text key="scko.error.config_circ_matrix_circ_mod_test" 
+  text="The patron has too many items of this type checked out"></eg-string>
+<eg-string i18n-text key="scko.error.config_circ_matrix_test_available_copy_hold_ratio" 
+  text="The available item-to-hold ratio is too low"></eg-string>
+<eg-string i18n-text key="scko.error.config_circ_matrix_test_circulate"
+  text="Circulation rules reject this item as non-circulatable"></eg-string>
+<eg-string i18n-text key="scko.error.config_circ_matrix_test_total_copy_hold_ratio"
+  text="The total item-to-hold ratio is too low"></eg-string>
+<eg-string i18n-text key="scko.error.config_hold_matrix_test_holdable"
+  text="Hold rules reject this item as unholdable"></eg-string>
+<eg-string i18n-text key="scko.error.config_hold_matrix_test_max_holds"
+  text="The patron has reached the maximum number of holds"></eg-string>
+<eg-string i18n-text key="scko.error.item.holdable"
+  text="The item is not holdable"></eg-string>
+<eg-string i18n-text key="scko.error.location.holdable"
+  text="The item's location is not holdable"></eg-string>
+<eg-string i18n-text key="scko.error.status.holdable"
+  text="The item is not in a holdable status"></eg-string>
+<eg-string i18n-text key="scko.error.config_rule_age_hold_protect_prox"
+  text="The item is too new to transit this far"></eg-string>
+<eg-string i18n-text key="scko.error.no_item"
+  text="The system could not find this item"></eg-string>
+<eg-string i18n-text key="scko.error.no_ultimate_items"
+  text="The system could not find any items to match this hold request"></eg-string>
+<eg-string i18n-text key="scko.error.no_matchpoint"
+  text="System rules do not define how to handle this item"></eg-string>
+<eg-string i18n-text key="scko.error.no_user"
+  text="The system could not find this patron"></eg-string>
+<eg-string i18n-text key="scko.error.transit_range"
+  text="The item cannot transit this far"></eg-string>
+<eg-string i18n-text key="scko.error.patron_exceeds_checkout_count"
+  text="Maximum checkouts reached on this account"></eg-string>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/scko.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/scko.component.ts
new file mode 100644 (file)
index 0000000..1cea153
--- /dev/null
@@ -0,0 +1,43 @@
+import {Component, OnInit, AfterViewInit, ViewChild, ViewEncapsulation} from '@angular/core';
+import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {SckoService} from './scko.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+
+@Component({
+  templateUrl: 'scko.component.html',
+  styleUrls: ['scko.component.css'],
+  encapsulation: ViewEncapsulation.None
+})
+
+export class SckoComponent implements OnInit, AfterViewInit {
+
+    @ViewChild('logoutDialog') logoutDialog: ConfirmDialogComponent;
+    @ViewChild('alertDialog') alertDialog: ConfirmDialogComponent;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private auth: AuthService,
+        public  scko: SckoService
+    ) {}
+
+    ngOnInit() {
+        this.net.authExpired$.subscribe(how => {
+            console.debug('SCKO auth expired with info', how);
+            this.scko.logoutStaff();
+        });
+
+        this.scko.load();
+    }
+
+    ngAfterViewInit() {
+        this.scko.logoutDialog = this.logoutDialog;
+        this.scko.alertDialog = this.alertDialog;
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/scko.module.ts b/Open-ILS/src/eg2/src/app/staff/scko/scko.module.ts
new file mode 100644 (file)
index 0000000..da4f188
--- /dev/null
@@ -0,0 +1,41 @@
+import {NgModule} from '@angular/core';
+import {EgCommonModule} from '@eg/common.module';
+import {CommonWidgetsModule} from '@eg/share/common-widgets.module';
+import {AudioService} from '@eg/share/util/audio.service';
+import {TitleComponent} from '@eg/share/title/title.component';
+import {PatronModule} from '@eg/staff/share/patron/patron.module';
+
+import {SckoComponent} from './scko.component';
+import {SckoRoutingModule} from './routing.module';
+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';
+import {SckoHoldsComponent} from './holds.component';
+import {SckoFinesComponent} from './fines.component';
+
+@NgModule({
+  declarations: [
+    SckoComponent,
+    SckoBannerComponent,
+    SckoSummaryComponent,
+    SckoCheckoutComponent,
+    SckoItemsComponent,
+    SckoHoldsComponent,
+    SckoFinesComponent
+  ],
+  imports: [
+    EgCommonModule,
+    CommonWidgetsModule,
+    PatronModule,
+    SckoRoutingModule
+  ],
+  providers: [
+    SckoService,
+    AudioService
+  ]
+})
+
+export class SckoModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/scko.service.ts b/Open-ILS/src/eg2/src/app/staff/scko/scko.service.ts
new file mode 100644 (file)
index 0000000..1398fa9
--- /dev/null
@@ -0,0 +1,693 @@
+import {Injectable, EventEmitter} from '@angular/core';
+import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
+import {Observable} from 'rxjs';
+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 {PatronService, PatronSummary, PatronStats} from '@eg/staff/share/patron/patron.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {PrintService} from '@eg/share/print/print.service';
+import {AudioService} from '@eg/share/util/audio.service';
+import {StringService} from '@eg/share/string/string.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+interface CheckoutContext {
+    barcode: string; // item
+    result: any;
+    firstEvent: EgEvent;
+    payload: any;
+    override: boolean;
+    redo: boolean;
+    renew: boolean;
+    displayText: string; // string key
+    alertSound: string;
+    shouldPopup: boolean;
+    previousCirc?: IdlObject;
+    renewalFailure?: boolean;
+}
+
+interface SessionCheckout {
+    circ: IdlObject;
+    ctx: CheckoutContext;
+}
+
+const CIRC_FLESH_DEPTH = 4;
+const CIRC_FLESH_FIELDS = {
+  circ: ['target_copy'],
+  acp:  ['call_number'],
+  acn:  ['record'],
+  bre:  ['flat_display_entries']
+};
+
+@Injectable({providedIn: 'root'})
+export class SckoService {
+
+    // Currently active patron account object.
+    patronSummary: PatronSummary;
+    statusDisplayText = '';
+    statusDisplaySuccess: boolean;
+
+    barcodeRegex: RegExp;
+    patronPasswordRequired = false;
+    patronIdleTimeout: number;
+    patronTimeoutId: number;
+    logoutWarningTimeout = 20;
+    logoutWarningTimerId: number;
+
+    alertAudio = false;
+    alertPopup = false;
+    orgSettings: any;
+    overrideCheckoutEvents: string[] = [];
+    blockStatuses: number[] = [];
+
+    sessionCheckouts: SessionCheckout[] = [];
+
+    // We get this from the main scko component.
+    logoutDialog: ConfirmDialogComponent;
+    alertDialog: AlertDialogComponent;
+    focusBarcode: EventEmitter<void> = new EventEmitter<void>();
+    patronLoaded: EventEmitter<void> = new EventEmitter<void>();
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private org: OrgService,
+        private net: NetService,
+        private evt: EventService,
+        public auth: AuthService,
+        private pcrud: PcrudService,
+        private printer: PrintService,
+        private audio: AudioService,
+        private strings: StringService,
+        private patrons: PatronService,
+    ) {}
+
+    logoutStaff() {
+        this.resetPatron();
+        this.auth.logout();
+        this.router.navigate(['/staff/scko']);
+    }
+
+    resetPatron() {
+        this.statusDisplayText = '';
+        this.patronSummary = null;
+        this.sessionCheckouts = [];
+    }
+
+    load(): Promise<any> {
+        this.auth.authDomain = 'eg.scko';
+
+        return this.auth.testAuthToken()
+
+        .then(_ => {
+
+            // 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',
+                'circ.selfcheck.patron_password_required',
+                'circ.checkout_auto_renew_age',
+                'circ.selfcheck.workstation_required',
+                'circ.selfcheck.alert.popup',
+                'circ.selfcheck.alert.sound',
+                'credit.payments.allow',
+                'circ.selfcheck.block_checkout_on_copy_status'
+            ]);
+
+        }).then(sets => {
+            this.orgSettings = sets;
+
+            const regPattern = sets['opac.barcode_regex'] || /^\d/;
+            this.barcodeRegex = new RegExp(regPattern);
+            this.patronPasswordRequired =
+                sets['circ.selfcheck.patron_password_required'];
+
+            this.alertAudio = sets['circ.selfcheck.alert.sound'];
+            this.alertPopup = sets['circ.selfcheck.alert.popup'];
+
+            this.overrideCheckoutEvents =
+                sets['circ.selfcheck.auto_override_checkout_events'] || [];
+
+            this.blockStatuses =
+                sets['circ.selfcheck.block_checkout_on_copy_status'] ?
+                sets['circ.selfcheck.block_checkout_on_copy_status'].map(s => Number(s)) :
+                [];
+
+            this.patronIdleTimeout =
+                Number(sets['circ.selfcheck.patron_login_timeout'] || 160);
+
+            // Compensate for the warning dialog
+            this.patronIdleTimeout -= this.logoutWarningTimeout;
+
+            // 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);
+            }
+        }).catch(_ => {}); // console errors
+    }
+
+    getFleshedCircs(circIds: number[]): Observable<IdlObject> {
+        return this.pcrud.search('circ', {id: circIds}, {
+            flesh: CIRC_FLESH_DEPTH,
+            flesh_fields: CIRC_FLESH_FIELDS,
+            order_by : {circ : 'due_date'},
+            select: {bre : ['id']}
+        });
+    }
+
+    getFleshedCirc(circId: number): Promise<IdlObject> {
+        return this.getFleshedCircs([circId]).toPromise();
+    }
+
+    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)
+        .then(_ => this.resetPatronTimeout())
+        .then(_ => this.patronLoaded.emit());
+    }
+
+    resetPatronTimeout() {
+        console.debug('Resetting patron timeout=' + this.patronIdleTimeout);
+        if (this.patronTimeoutId) {
+            clearTimeout(this.patronTimeoutId);
+        }
+        this.startPatronTimer();
+    }
+
+    startPatronTimer() {
+        this.patronTimeoutId = setTimeout(
+            () => this.showPatronLogoutWarning(),
+            this.patronIdleTimeout * 1000
+        );
+    }
+
+    showPatronLogoutWarning() {
+        console.debug('Session timing out.  Show warning dialog');
+
+        this.logoutDialog.open().subscribe(remain => {
+            if (remain) {
+                clearTimeout(this.logoutWarningTimerId);
+                this.logoutWarningTimerId = null;
+                this.resetPatronTimeout();
+            } else {
+                this.resetPatron();
+                this.router.navigate(['/staff/scko']);
+            }
+        });
+
+        // Force the session to end if no action is taken on the
+        // logout warning dialog.
+        this.logoutWarningTimerId = setTimeout(
+            () => {
+                console.debug('Clearing patron on warning dialog timeout');
+                this.resetPatron();
+                this.router.navigate(['/staff/scko']);
+            },
+            this.logoutWarningTimeout * 1000
+        );
+    }
+
+    sessionTotalCheckouts(): number {
+        return this.sessionCheckouts.length;
+    }
+
+    accountTotalCheckouts(): number {
+        // stats.checkouts.total_out includes claims returned
+
+        return this.sessionTotalCheckouts() +
+            this.patronSummary.stats.checkouts.out +
+            this.patronSummary.stats.checkouts.overdue +
+            this.patronSummary.stats.checkouts.long_overdue;
+    }
+
+
+    checkout(barcode: string, override?: boolean): Promise<any> {
+        this.resetPatronTimeout();
+
+        barcode = (barcode || '').trim();
+        if (!barcode) { return Promise.resolve(); }
+
+        let method = 'open-ils.circ.checkout.full';
+        if (override) { method += '.override'; }
+
+        return this.net.request(
+            'open-ils.circ', method, this.auth.token(), {
+            patron_id: this.patronSummary.id,
+            copy_barcode: barcode
+        }).toPromise()
+
+        .then(result => {
+
+            console.debug('CO returned', result);
+
+            return this.handleCheckoutResult(result, barcode, 'checkout');
+
+        }).then(ctx => {
+            console.debug('handleCheckoutResult returned', ctx);
+
+            if (ctx.override) {
+                return this.checkout(barcode, true);
+            } else if (ctx.redo) {
+                return this.checkout(barcode);
+            } else if (ctx.renew) {
+                return this.renew(barcode);
+            }
+
+            return ctx;
+
+        // Checkout actions always takes us back to the main page
+        // so we can see our items out in progress.
+        })
+        .then(ctx => this.notifyPatron(ctx))
+        .finally(() => this.router.navigate(['/staff/scko']));
+    }
+
+    renew(barcode: string, override?: boolean): Promise<any> {
+
+        let method = 'open-ils.circ.renew';
+        if (override) { method += '.override'; }
+
+        return this.net.request(
+            'open-ils.circ', method, this.auth.token(), {
+            patron_id: this.patronSummary.id,
+            copy_barcode: barcode
+        }).toPromise()
+
+        .then(result => {
+            console.debug('Renew returned', result);
+
+            return this.handleCheckoutResult(result, barcode, 'renew');
+
+        }).then(ctx => {
+            console.debug('handleCheckoutResult returned', ctx);
+
+            if (ctx.override) {
+                return this.renew(barcode, true);
+            }
+        });
+    }
+
+    notifyPatron(ctx: CheckoutContext) {
+        console.debug('notifyPatron(): ', ctx);
+
+        this.statusDisplayText = '';
+
+        this.statusDisplaySuccess = !ctx.shouldPopup;
+
+        this.focusBarcode.emit();
+
+        // TODO on success tell the summary to update its numbers.
+
+        if (this.alertAudio && ctx.alertSound) {
+            this.audio.play(ctx.alertSound);
+        }
+
+        if (!ctx.displayText) { return; }
+
+        this.strings.interpolate(ctx.displayText, {barcode: ctx.barcode})
+        .then(str => {
+            this.statusDisplayText = str;
+
+            if (this.alertPopup && ctx.shouldPopup && str) {
+                this.alertDialog.dialogBody = str;
+                this.alertDialog.open().toPromise();
+            }
+        });
+    }
+
+    handleCheckoutResult(
+        result: any, barcode: string, action: string): Promise<CheckoutContext> {
+
+        if (Array.isArray(result)) {
+            result = result[0];
+        }
+
+        const evt: any = this.evt.parse(result) || {};
+        const payload = evt.payload || {};
+
+        if (evt.textcode === 'NO_SESSION') {
+            this.logoutStaff();
+            return;
+        }
+
+        const ctx: CheckoutContext = {
+            result: result,
+            firstEvent: evt,
+            payload: payload,
+            barcode: barcode,
+            displayText: 'scko.unknown',
+            alertSound: '',
+            shouldPopup: false,
+            redo: false,
+            override: false,
+            renew: false
+        };
+
+        if (evt.textcode === 'SUCCESS') {
+            ctx.displayText = `scko.${action}.success`;
+            ctx.alertSound = `success.scko.${action}`;
+
+            return this.getFleshedCirc(payload.circ.id()).then(
+                circ => {
+                    this.sessionCheckouts.push({circ: circ, ctx: ctx});
+                    return ctx;
+                }
+            );
+        }
+
+        if (evt.textcode === 'OPEN_CIRCULATION_EXISTS' && action === 'checkout') {
+            return this.handleOpenCirc(ctx);
+        }
+
+        return this.handleEvents(ctx);
+    }
+
+    handleOpenCirc(ctx: CheckoutContext): Promise<any> {
+
+        if (ctx.payload.old_circ) {
+            const age = this.orgSettings['circ.checkout_auto_renew_age'];
+
+            if (!age || (age && ctx.payload.auto_renew)) {
+                ctx.renew = true;
+
+                // Flesh the previous circ so we can show the title,
+                // etc. in the receipt.
+                return this.getFleshedCirc(ctx.payload.old_circ.id())
+                .then(oldCirc => {
+                    ctx.previousCirc = oldCirc;
+                    return ctx;
+                });
+            }
+        }
+
+        // LOST items can be checked in and made usable if configured.
+        if (ctx.payload.copy
+            && Number(ctx.payload.copy.status()) === /* LOST */ 3
+            && this.overrideCheckoutEvents.length
+            && this.overrideCheckoutEvents.includes('COPY_STATUS_LOST')) {
+
+            return this.checkin(ctx.barcode).then(ok => {
+                if (ok) {
+                    ctx.redo = true;
+                } else {
+                    ctx.shouldPopup = true;
+                    ctx.alertSound = 'error.scko.checkout';
+                    ctx.displayText = 'scko.checkout.already_out';
+                }
+
+                return ctx;
+            });
+        }
+
+        ctx.shouldPopup = true;
+        ctx.alertSound = 'error.scko.checkout';
+        ctx.displayText = 'scko.checkout.already_out';
+
+        return Promise.resolve(ctx);
+    }
+
+    handleEvents(ctx: CheckoutContext): Promise<CheckoutContext> {
+        let override = true;
+        let abortTransit = false;
+        let lastErrorText = '';
+
+        [].concat(ctx.result).some(res => {
+
+            if (!this.overrideCheckoutEvents.includes(res.textcode)) {
+                console.debug('We are not configured to override', res.textcode);
+                lastErrorText = this.getErrorDisplyText(this.evt.parse(res));
+                return override = false;
+            }
+
+            if (this.blockStatuses.length > 0) {
+                let stat = res.payload.status();
+                if (typeof stat === 'object') { stat = stat.id(); }
+
+                if (this.blockStatuses.includes(Number(stat))) {
+                    return override = false;
+                }
+            }
+
+            if (res.textcode === 'COPY_IN_TRANSIT') {
+                abortTransit = true;
+            }
+
+            return true;
+        });
+
+        if (!override) {
+            ctx.shouldPopup = true;
+            ctx.alertSound = 'error.scko.checkout';
+            ctx.renewalFailure = true;
+            ctx.displayText = lastErrorText;
+            return Promise.resolve(ctx);
+        }
+
+        if (!abortTransit) {
+            ctx.override = true;
+            return Promise.resolve(ctx);
+        }
+
+        return this.checkin(ctx.barcode, true).then(ok => {
+            if (ok) {
+                ctx.redo = true;
+            } else {
+                ctx.shouldPopup = true;
+                ctx.alertSound = 'error.scko.checkout';
+            }
+            return ctx;
+        });
+    }
+
+    getErrorDisplyText(evt: EgEvent): string {
+
+        switch (evt.textcode) {
+            case 'PATRON_EXCEEDS_CHECKOUT_COUNT':
+                return 'scko.error.patron_exceeds_checkout_count';
+            case 'MAX_RENEWALS_REACHED':
+                return 'scko.error.max_renewals';
+            case 'ITEM_NOT_CATALOGED':
+                return 'scko.error.item_not_cataloged';
+            case 'COPY_CIRC_NOT_ALLOWED':
+                return 'scko.error.copy_circ_not_allowed';
+            case 'OPEN_CIRCULATION_EXISTS':
+                return 'scko.error.already_out';
+            case 'PATRON_EXCEEDS_FINES':
+                return 'scko.error.patron_fines';
+            default:
+                if (evt.payload && evt.payload.fail_part) {
+                    return 'scko.error.' +
+                        evt.payload.fail_part.replace(/\./g, '_');
+                }
+        }
+
+        return 'scko.error.unknown';
+    }
+
+    checkin(barcode: string, abortTransit?: boolean): Promise<boolean> {
+
+        let promise = Promise.resolve(true);
+
+        if (abortTransit) {
+
+            promise = this.net.request(
+                'open-ils.circ',
+                'open-ils.circ.transit.abort',
+                this.auth.token(), {barcode: barcode}).toPromise()
+
+            .then(resp => {
+
+                console.debug('Transit abort returned', resp);
+                return Number(resp) === 1;
+            });
+        }
+
+        promise = promise.then(ok => {
+            if (!ok) { return false; }
+
+            return this.net.request(
+                'open-ils.circ',
+                'open-ils.circ.checkin.override',
+                this.auth.token(), {
+                    patron_id : this.patronSummary.id,
+                    copy_barcode : barcode,
+                    noop : true
+                }
+
+            ).toPromise().then(resp => {
+
+                // If any response events are non-success, report the
+                // checkin as a failure.
+                let success = true;
+                [].concat(resp).forEach(evt => {
+                    console.debug('Checkin returned', resp);
+
+                    const code = evt.textcode;
+                    if (code !== 'SUCCESS' && code !== 'NO_CHANGE') {
+                        success = false;
+                    }
+                });
+
+                return success;
+
+            });
+        });
+
+        return promise;
+    }
+
+    logoutPatron(receiptType: string): Promise<any> {
+
+        let promise;
+
+        switch (receiptType) {
+            case 'email':
+                promise = this.emailReceipt();
+                break;
+            case 'print':
+                promise = this.printReceipt();
+                break;
+            default:
+                promise = Promise.resolve();
+        }
+
+        return promise.then(_ => {
+            this.resetPatron();
+            this.router.navigate(['/staff/scko']);
+        });
+    }
+
+    emailReceipt(): Promise<any> {
+
+        const circIds = this.sessionCheckouts
+            .filter(c => Boolean(c.circ)).map(c => c.circ.id());
+
+        return this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.checkout.batch_notify.session.atomic',
+            this.auth.token(), this.patronSummary.id, circIds
+        ).toPromise();
+    }
+
+    printReceipt(): Promise<any> {
+
+        return new Promise((resolve, reject) => {
+
+            const sub = this.printer.printJobQueued$.subscribe(_ => {
+                sub.unsubscribe();
+                // Give the print operation just a bit more time after
+                // the data is passed to the printer just to be safe.
+                setTimeout(() => resolve(null), 1000);
+            });
+
+            const data = this.sessionCheckouts.map(c => {
+                const circ = c.circ || c.ctx.previousCirc;
+                return {
+                    checkout: c,
+                    barcode: c.ctx.barcode,
+                    circ: circ,
+                    copy: circ ? circ.target_copy() : null,
+                    title: this.getCircTitle(circ),
+                    author: this.getCircAuthor(circ)
+                };
+            });
+
+            this.printer.print({
+                templateName: 'scko_checkouts',
+                contextData: {
+                    checkouts: data,
+                    user: this.patronSummary.patron
+                },
+                printContext: 'default'
+            });
+        });
+    }
+
+    copyIsPrecat(copy: IdlObject): boolean {
+        return Number(copy.id()) === -1;
+    }
+
+    circDisplayValue(circ: IdlObject, field: string): string {
+        if (!circ) { return ''; }
+
+        const entry =
+            circ.target_copy().call_number().record().flat_display_entries()
+            .filter(e => e.name() === field)[0];
+
+        return entry ? entry.value() : '';
+    }
+
+    getCircTitle(circ: IdlObject): string {
+        if (!circ) { return ''; }
+        const copy = circ.target_copy();
+        if (this.copyIsPrecat(copy)) { return copy.dummy_title(); }
+        return this.circDisplayValue(circ, 'title');
+    }
+
+    getCircAuthor(circ: IdlObject): string {
+        if (!circ) { return ''; }
+        const copy = circ.target_copy();
+        if (this.copyIsPrecat(copy)) { return copy.dummy_author(); }
+        return this.circDisplayValue(circ, 'author');
+    }
+
+}
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/summary.component.html b/Open-ILS/src/eg2/src/app/staff/scko/summary.component.html
new file mode 100644 (file)
index 0000000..7ca92bb
--- /dev/null
@@ -0,0 +1,91 @@
+
+<div id="scko-circ-info-div">
+
+  <div class="pl-2">
+    <span>
+      <a routerLink="/staff/scko" class="mr-2">
+        <button type="button" class="scko-button">Home</button>
+      </a>
+    </span>
+    <span>
+      <a (click)="scko.logoutPatron(receiptType)">
+        <button type="button" class="scko-button">Logout</button>
+      </a>
+    </span>
+  </div>
+
+  <div class="pl-2 mt-3">
+    <span class="mr-2" i18n>Receipt: </span>
+
+    <div *ngIf="showEmailOption" class="form-check form-check-inline">
+      <input class="form-check-input" name="receipt-type" type="radio" 
+        [(ngModel)]="receiptType" id="receipt-email" value="email">
+      <label class="form-check-label" for="receipt-email" i18n>Email</label>
+    </div>
+
+    <div class="form-check form-check-inline">
+      <input class="form-check-input" name="receipt-type" type="radio" 
+        [(ngModel)]="receiptType" id="receipt-print" value="print">
+      <label class="form-check-label" for="receipt-print" i18n>Print</label>
+    </div>
+
+    <div class="form-check form-check-inline">
+      <input class="form-check-input" name="receipt-type" type="radio" 
+        [(ngModel)]="receiptType" id="receipt-none" value="none">
+      <label class="form-check-label" for="receipt-none" i18n>None</label>
+    </div>
+  </div>
+
+  <fieldset>
+    <legend i18n>Items Checked Out</legend>
+    <div>
+      <span i18n>Total items this session: </span>
+      <span class="font-weight-bold">{{scko.sessionTotalCheckouts()}}</span>
+    </div>
+    <div class="mt-2">
+      <span i18n>Total items on account: </span>
+      <span class="font-weight-bold">{{scko.accountTotalCheckouts()}}</span>
+    </div>
+    <div class="mt-2">
+      <a routerLink="/staff/scko/items">
+        <button type="button" class="scko-button" i18n>View Items Out</button>
+      </a>
+    </div>
+  </fieldset>
+  <fieldset>
+    <legend i18n>Holds</legend>
+    <div i18n>
+      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 
+      <span class="font-weight-bold">{{scko.patronSummary.stats.holds.total}}</span>
+      total holds.
+    </div>
+    <div class="mt-2">
+      <a routerLink="/staff/scko/holds" id="oils-selfck-hold-details-link">
+        <button type="button" class="scko-button" i18n>View Holds</button>
+      </a>
+    </div>
+  </fieldset>
+  <fieldset>
+    <legend i18n>Fines</legend>
+    <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 routerLink="/staff/scko/fines" id="oils-selfck-view-fines-link">
+          <button type="button" class="scko-button" i18n>View Details</button>
+        </a>
+      </span>
+    </div>
+  </fieldset>
+</div>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/summary.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/summary.component.ts
new file mode 100644 (file)
index 0000000..454cabe
--- /dev/null
@@ -0,0 +1,53 @@
+import {Component, OnInit, NgZone, HostListener} from '@angular/core';
+import {Location} from '@angular/common';
+import {Router, ActivatedRoute, NavigationEnd} from '@angular/router';
+import {AuthService, AuthWsState} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {StoreService} from '@eg/core/store.service';
+import {SckoService} from './scko.service';
+import {OrgService} from '@eg/core/org.service';
+import {EventService, EgEvent} from '@eg/core/event.service';
+
+@Component({
+  selector: 'eg-scko-summary',
+  templateUrl: 'summary.component.html'
+})
+
+export class SckoSummaryComponent implements OnInit {
+
+    showEmailOption = false;
+    receiptType = 'email';
+
+    constructor(
+        public scko: SckoService
+    ) {}
+
+    ngOnInit() {
+        this.scko.patronLoaded.subscribe(() => {
+            if (this.canEmail()) {
+                this.showEmailOption = true;
+                this.receiptType = 'email';
+            } else {
+                this.showEmailOption = false;
+                this.receiptType = 'print';
+            }
+        });
+    }
+
+    canEmail(): boolean {
+        if (!this.scko.patronSummary) { return false; }
+
+        const patron = this.scko.patronSummary.patron;
+
+        const setting = patron.settings().filter(
+            s => s.name() === 'circ.send_email_checkout_receipts')[0];
+
+        return (
+            Boolean(patron.email())
+            && patron.email().match(/.*@.*/) !== null
+            && setting
+            && setting.value() === 'true' // JSON
+        );
+    }
+}
+