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)
}
];
+++ /dev/null
-
-<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>
+++ /dev/null
-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);
- }
-}
-
+++ /dev/null
-<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>
+++ /dev/null
-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() {
- }
-}
-
+++ /dev/null
-<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>
+++ /dev/null
-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'
- });
- }
-}
-
+++ /dev/null
-<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>
+++ /dev/null
-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'
- });
- }
-}
-
-
+++ /dev/null
-<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>
+++ /dev/null
-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'
- });
- }
-}
-
-
-
+++ /dev/null
-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 {}
-
+++ /dev/null
-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;
-}
+++ /dev/null
-
-<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>
-
+++ /dev/null
-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;
- }
-}
-
+++ /dev/null
-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 {}
-
+++ /dev/null
-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');
- }
-
-}
-
-
-
+++ /dev/null
-
-<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>
-
-
+++ /dev/null
-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
- );
- }
-}
-
--- /dev/null
+
+<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>
--- /dev/null
+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);
+ }
+}
+
--- /dev/null
+<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>
--- /dev/null
+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() {
+ }
+}
+
--- /dev/null
+<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>
--- /dev/null
+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'
+ });
+ }
+}
+
--- /dev/null
+<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>
--- /dev/null
+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'
+ });
+ }
+}
+
+
--- /dev/null
+<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>
--- /dev/null
+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'
+ });
+ }
+}
+
+
+
--- /dev/null
+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 {}
+
--- /dev/null
+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;
+}
--- /dev/null
+
+<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>
+
--- /dev/null
+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;
+ }
+}
+
--- /dev/null
+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 {}
+
--- /dev/null
+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');
+ }
+
+}
+
+
+
--- /dev/null
+
+<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>
+
+
--- /dev/null
+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
+ );
+ }
+}
+