From: Bill Erickson Date: Thu, 7 Jul 2022 14:55:15 +0000 (-0400) Subject: LP1840773 SCKO Angular Hatch Accessible X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=7d625bcb45a8c8959ba618bab9f4ebfd06d94104;p=working%2FEvergreen.git LP1840773 SCKO Angular Hatch Accessible Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/eg2/src/app/routing.module.ts b/Open-ILS/src/eg2/src/app/routing.module.ts index 087c0cc3ad..c440a3a860 100644 --- a/Open-ILS/src/eg2/src/app/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/routing.module.ts @@ -18,9 +18,9 @@ const routes: Routes = [ resolve : {startup : BaseResolver}, loadChildren: () => import('./staff/staff.module').then(m => m.StaffModule) }, { - path: 'scko', + path: 'staff/scko', resolve : {startup : BaseResolver}, - loadChildren: () => import('./scko/scko.module').then(m => m.SckoModule) + loadChildren: () => import('./staff/scko/scko.module').then(m => m.SckoModule) } ]; diff --git a/Open-ILS/src/eg2/src/app/scko/banner.component.html b/Open-ILS/src/eg2/src/app/scko/banner.component.html deleted file mode 100644 index cea5bd0ecb..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/banner.component.html +++ /dev/null @@ -1,132 +0,0 @@ - -
-
- -
-
- -
- Please log in with your username or library barcode. -
-
-
-
-
- - - - - - - - - - -
-
-
-
-
- - -
Please enter an item barcode
-
-
-
-
- - - - - -
-
-
-
-
Welcome, - {{scko.patronSummary.patron.pref_first_given_name() - || scko.patronSummary.patron.first_given_name()}} -
-
-
-
- -
-
- -
- -
-
-

Staff Account Login

-
-
- -
- - -
- -
- - -
- -
- - -
- -
-
- -
-
-
Login Failed
-
-
-
-
-
-
diff --git a/Open-ILS/src/eg2/src/app/scko/banner.component.ts b/Open-ILS/src/eg2/src/app/scko/banner.component.ts deleted file mode 100644 index 4485f86827..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/banner.component.ts +++ /dev/null @@ -1,142 +0,0 @@ -import {Component, OnInit, AfterViewInit, NgZone, HostListener} from '@angular/core'; -import {Location} from '@angular/common'; -import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; -import {AuthService, AuthWsState} from '@eg/core/auth.service'; -import {NetService} from '@eg/core/net.service'; -import {StoreService} from '@eg/core/store.service'; -import {SckoService} from './scko.service'; -import {OrgService} from '@eg/core/org.service'; -import {EventService, EgEvent} from '@eg/core/event.service'; - -@Component({ - selector: 'eg-scko-banner', - templateUrl: 'banner.component.html' -}) - -export class SckoBannerComponent implements OnInit, AfterViewInit { - - workstations: any[]; - workstationNotFound = false; - - patronUsername: string; - patronPassword: string; - patronLoginFailed = false; - - staffUsername: string; - staffPassword: string; - staffWorkstation: string; - staffLoginFailed = false; - - itemBarcode: string; - - constructor( - private route: ActivatedRoute, - private store: StoreService, - private net: NetService, - private auth: AuthService, - private evt: EventService, - private ngLocation: Location, - private org: OrgService, - public scko: SckoService - ) {} - - ngOnInit() { - - // NOTE: Displaying a list of workstations will not work for users - // of Hatch until the extension is updated to support /eg2/*/scko - this.store.getWorkstations() - .then(wsList => { - this.workstations = wsList; - return this.store.getDefaultWorkstation(); - }).then(def => { - this.staffWorkstation = def; - this.applyWorkstation(); - }); - } - - ngAfterViewInit() { - if (this.auth.token()) { - this.focusNode('patron-username'); - } else { - this.focusNode('staff-username'); - } - - this.scko.focusBarcode.subscribe(_ => this.focusNode('item-barcode')); - } - - focusNode(id: string) { - setTimeout(() => { - const node = document.getElementById(id); - if (node) { (node as HTMLInputElement).select(); } - }); - } - - applyWorkstation() { - const wanted = this.route.snapshot.queryParamMap.get('workstation'); - if (!wanted) { return; } // use the default - - const exists = this.workstations.filter(w => w.name === wanted)[0]; - if (exists) { - this.staffWorkstation = wanted; - } else { - console.error(`Unknown workstation requested: ${wanted}`); - } - } - - submitStaffLogin() { - - this.staffLoginFailed = false; - - const args = { - type: 'persistent', - username: this.staffUsername, - password: this.staffPassword, - workstation: this.staffWorkstation - }; - - this.staffLoginFailed = false; - this.workstationNotFound = false; - - this.auth.login(args).then( - ok => { - - if (this.auth.workstationState === AuthWsState.NOT_FOUND_SERVER) { - this.staffLoginFailed = true; - this.workstationNotFound = true; - - } else { - - // Initial login clears cached org unit setting values - // and user/workstation setting values - this.org.clearCachedSettings().then(_ => { - - // Force reload of the app after a successful login. - window.location.href = - this.ngLocation.prepareExternalUrl('/scko'); - - }); - } - }, - notOk => { - this.staffLoginFailed = true; - } - ); - } - - submitPatronLogin() { - this.patronLoginFailed = false; - this.scko.loadPatron(this.patronUsername, this.patronPassword).finally(() => { - if (this.scko.patronSummary === null) { - this.patronLoginFailed = true; - } else { - this.focusNode('item-barcode'); - } - }); - } - - submitItemBarcode() { - this.scko.resetPatronTimeout(); - this.scko.checkout(this.itemBarcode); - } -} - diff --git a/Open-ILS/src/eg2/src/app/scko/checkout.component.html b/Open-ILS/src/eg2/src/app/scko/checkout.component.html deleted file mode 100644 index 51f549f5a4..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/checkout.component.html +++ /dev/null @@ -1,35 +0,0 @@ -
- - - - - - - - - - - - - - - - - - - - - - - -
BarcodeTitleAuthorDue DateRenewals LeftType
- - - - {{co.circ.target_copy().barcode()}}{{scko.getCircTitle(co.circ)}}{{scko.getCircAuthor(co.circ)}}{{co.circ | egDueDate}}{{co.circ.renewal_remaining()}} - - Renewal - Checkout - -
-
diff --git a/Open-ILS/src/eg2/src/app/scko/checkout.component.ts b/Open-ILS/src/eg2/src/app/scko/checkout.component.ts deleted file mode 100644 index 5afc1f6f29..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/checkout.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {Component, OnInit, ViewEncapsulation} from '@angular/core'; -import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; -import {AuthService} from '@eg/core/auth.service'; -import {IdlObject} from '@eg/core/idl.service'; -import {SckoService} from './scko.service'; -import {ServerStoreService} from '@eg/core/server-store.service'; - -@Component({ - templateUrl: 'checkout.component.html' -}) - -export class SckoCheckoutComponent implements OnInit { - - constructor( - private router: Router, - private route: ActivatedRoute, - public scko: SckoService - ) {} - - ngOnInit() { - } -} - diff --git a/Open-ILS/src/eg2/src/app/scko/fines.component.html b/Open-ILS/src/eg2/src/app/scko/fines.component.html deleted file mode 100644 index d4444c3cb1..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/fines.component.html +++ /dev/null @@ -1,35 +0,0 @@ -
-
-
- -
-
-
- - - - - - - - - - - - - - - - - - - -
TypeDetailsTotal BilledTotal PaidBalance Owed
- - Circulation - - - Miscellaneous - - {{getDetails(xact)}}{{xact.summary().total_owed() | currency}}{{xact.summary().total_paid() | currency}}{{xact.summary().balance_owed() | currency}}
-
diff --git a/Open-ILS/src/eg2/src/app/scko/fines.component.ts b/Open-ILS/src/eg2/src/app/scko/fines.component.ts deleted file mode 100644 index f13bda7615..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/fines.component.ts +++ /dev/null @@ -1,110 +0,0 @@ -import {Component, OnInit, ViewEncapsulation} from '@angular/core'; -import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; -import {switchMap, tap} from 'rxjs/operators'; -import {AuthService} from '@eg/core/auth.service'; -import {PcrudService} from '@eg/core/pcrud.service'; -import {NetService} from '@eg/core/net.service'; -import {IdlObject} from '@eg/core/idl.service'; -import {SckoService} from './scko.service'; -import {PrintService} from '@eg/share/print/print.service'; - - -@Component({ - templateUrl: 'fines.component.html' -}) - -export class SckoFinesComponent implements OnInit { - - xacts: IdlObject[] = []; - - constructor( - private router: Router, - private route: ActivatedRoute, - private net: NetService, - private auth: AuthService, - private pcrud: PcrudService, - private printer: PrintService, - public scko: SckoService - ) {} - - ngOnInit() { - - if (!this.scko.patronSummary) { - this.router.navigate(['/scko']); - return; - } - - this.scko.resetPatronTimeout(); - - this.pcrud.search('mbts', - { usr: this.scko.patronSummary.id, - xact_finish: null, - balance_owed: {'<>' : 0} - }, {}, {atomic: true} - ).pipe(switchMap(sums => { - - return this.pcrud.search('mbt', {id: sums.map(s => s.id())}, - { order_by: 'xact_start', - flesh: 5, - flesh_fields: { - mbt: ['summary', 'circulation', 'grocery'], - circ: ['target_copy'], - acp: ['call_number'], - acn: ['record'], - bre: ['flat_display_entries'] - }, - select: {bre : ['id']} - } - ).pipe(tap(xact => this.xacts.push(xact))); - })).toPromise(); - } - - displayValue(xact: IdlObject, field: string): string { - const entry = - xact.circulation().target_copy().call_number().record().flat_display_entries() - .filter(e => e.name() === field)[0]; - - return entry ? entry.value() : ''; - } - - getTitle(xact: IdlObject): string { - const copy = xact.circulation().target_copy(); - - if (Number(copy.call_number().id()) === -1) { - return copy.dummy_title(); - } - - return this.displayValue(xact, 'title'); - } - - getDetails(xact: IdlObject): string { - if (xact.summary().xact_type() === 'circulation') { - return this.getTitle(xact); - } else { - return xact.summary().last_billing_type(); - } - } - - printList() { - - const data = this.xacts.map(x => { - return { - xact: x, // full object if needed - details: this.getDetails(x), - total_owed: x.summary().total_owed(), - total_paid: x.summary().total_paid(), - balance_owed: x.summary().balance_owed(), - }; - }); - - this.printer.print({ - templateName: 'scko_fines', - contextData: { - xacts: data, - user: this.scko.patronSummary.patron - }, - printContext: 'default' - }); - } -} - diff --git a/Open-ILS/src/eg2/src/app/scko/holds.component.html b/Open-ILS/src/eg2/src/app/scko/holds.component.html deleted file mode 100644 index ff10e9bc7d..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/holds.component.html +++ /dev/null @@ -1,33 +0,0 @@ -
-
-
- -
-
-
- - - - - - - - - - - - - - - - - -
TitleAuthorStatus
{{hold.title}}{{hold.author}} - - Ready for Pickup - - - #{{hold.relative_queue_position}} in line with {{hold.potentials}} copie(s) - -
-
diff --git a/Open-ILS/src/eg2/src/app/scko/holds.component.ts b/Open-ILS/src/eg2/src/app/scko/holds.component.ts deleted file mode 100644 index 131cf73a23..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/holds.component.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {Component, OnInit, ViewEncapsulation} from '@angular/core'; -import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; -import {tap} from 'rxjs/operators'; -import {AuthService} from '@eg/core/auth.service'; -import {PcrudService} from '@eg/core/pcrud.service'; -import {NetService} from '@eg/core/net.service'; -import {IdlObject} from '@eg/core/idl.service'; -import {SckoService} from './scko.service'; -import {ServerStoreService} from '@eg/core/server-store.service'; -import {PrintService} from '@eg/share/print/print.service'; - -@Component({ - templateUrl: 'holds.component.html' -}) - -export class SckoHoldsComponent implements OnInit { - - holds: IdlObject[] = []; - - constructor( - private router: Router, - private route: ActivatedRoute, - private net: NetService, - private auth: AuthService, - private pcrud: PcrudService, - private printer: PrintService, - public scko: SckoService - ) {} - - ngOnInit() { - - if (!this.scko.patronSummary) { - this.router.navigate(['/scko']); - return; - } - - this.scko.resetPatronTimeout(); - - const orderBy = [ - {shelf_time: {nulls: 'last'}}, - {capture_time: {nulls: 'last'}}, - {request_time: {nulls: 'last'}} - ]; - - const filters = { - usr_id: this.scko.patronSummary.id, - fulfillment_time: null - }; - - let first = true; - this.net.request( - 'open-ils.circ', - 'open-ils.circ.hold.wide_hash.stream', - this.auth.token(), filters, orderBy, 1000, 0, {} - ).subscribe(holdData => { - - if (first) { // First response is the hold count. - first = false; - return; - } - - this.holds.push(holdData); - }); - } - - printList() { - this.printer.print({ - templateName: 'scko_holds', - contextData: { - holds: this.holds, - user: this.scko.patronSummary.patron - }, - printContext: 'default' - }); - } -} - - diff --git a/Open-ILS/src/eg2/src/app/scko/items.component.html b/Open-ILS/src/eg2/src/app/scko/items.component.html deleted file mode 100644 index c4a247bedf..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/items.component.html +++ /dev/null @@ -1,39 +0,0 @@ -
-
-
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
BarcodeTitleAuthorDue DateRenewals LeftType
- - - - {{circ.target_copy().barcode()}}{{scko.getCircTitle(circ)}}{{scko.getCircAuthor(circ)}}{{circ | egDueDate}}{{circ.renewal_remaining()}} - Renewal - Checkout -
-
diff --git a/Open-ILS/src/eg2/src/app/scko/items.component.ts b/Open-ILS/src/eg2/src/app/scko/items.component.ts deleted file mode 100644 index 7a662a5245..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/items.component.ts +++ /dev/null @@ -1,75 +0,0 @@ -import {Component, OnInit, ViewEncapsulation} from '@angular/core'; -import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; -import {tap} from 'rxjs/operators'; -import {AuthService} from '@eg/core/auth.service'; -import {PcrudService} from '@eg/core/pcrud.service'; -import {NetService} from '@eg/core/net.service'; -import {IdlObject} from '@eg/core/idl.service'; -import {SckoService} from './scko.service'; -import {ServerStoreService} from '@eg/core/server-store.service'; -import {PrintService} from '@eg/share/print/print.service'; - -@Component({ - templateUrl: 'items.component.html' -}) - -export class SckoItemsComponent implements OnInit { - - circs: IdlObject[] = []; - - constructor( - private router: Router, - private route: ActivatedRoute, - private net: NetService, - private auth: AuthService, - private pcrud: PcrudService, - private printer: PrintService, - public scko: SckoService - ) {} - - ngOnInit() { - - if (!this.scko.patronSummary) { - this.router.navigate(['/scko']); - return; - } - - this.scko.resetPatronTimeout(); - - this.net.request( - 'open-ils.actor', - 'open-ils.actor.user.checked_out.authoritative', - this.auth.token(), this.scko.patronSummary.id).toPromise() - - .then(data => { - const ids = data.out.concat(data.overdue).concat(data.long_overdue); - return this.scko.getFleshedCircs(ids).pipe(tap(circ => { - this.circs.push(circ); - })).toPromise(); - }); - } - - printList() { - - const data = this.circs.map(c => { - return { - circ: c, - copy: c.target_copy(), - title: this.scko.getCircTitle(c), - author: this.scko.getCircAuthor(c) - }; - }); - - this.printer.print({ - templateName: 'scko_items_out', - contextData: { - checkouts: data, - user: this.scko.patronSummary.patron - }, - printContext: 'default' - }); - } -} - - - diff --git a/Open-ILS/src/eg2/src/app/scko/routing.module.ts b/Open-ILS/src/eg2/src/app/scko/routing.module.ts deleted file mode 100644 index 08814530a1..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/routing.module.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; -import {SckoComponent} from './scko.component'; -import {SckoCheckoutComponent} from './checkout.component'; -import {SckoItemsComponent} from './items.component'; -import {SckoHoldsComponent} from './holds.component'; -import {SckoFinesComponent} from './fines.component'; - -const routes: Routes = [{ - path: '', - component: SckoComponent, - children: [{ - path: '', - component: SckoCheckoutComponent - }, { - path: 'items', - component: SckoItemsComponent - }, { - path: 'holds', - component: SckoHoldsComponent - }, { - path: 'fines', - component: SckoFinesComponent - }] -}]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) - -export class SckoRoutingModule {} - diff --git a/Open-ILS/src/eg2/src/app/scko/scko.component.css b/Open-ILS/src/eg2/src/app/scko/scko.component.css deleted file mode 100644 index 0150ee34a1..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/scko.component.css +++ /dev/null @@ -1,126 +0,0 @@ -body { - font-family: Arial, Verdana; - font-size: 13px; -} - -A { - text-decoration: none; -} - -#scko-banner { - background: #00593d; /* Old browsers */ -/* IE9 SVG, needs conditional override of 'filter' to 'none' */ - background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwNTkzZCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMwMDdhNTQiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); - background: -moz-linear-gradient(top, #00593d 0%, #007a54 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00593d), color-stop(100%,#007a54)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #00593d 0%,#007a54 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #00593d 0%,#007a54 100%); /* Opera 11.10+ */ - background: -ms-linear-gradient(top, #00593d 0%,#007a54 100%); /* IE10+ */ - background: linear-gradient(to bottom, #00593d 0%,#007a54 100%); /* W3C */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00593d', endColorstr='#007a54',GradientType=0 ); /* IE6-8 */ - padding-top: 20px; - text-align: center; - font-weight:bold; -} - -#scko-scan-input-text { - font-size: 16px; - background: none repeat scroll 0 0 #252525; - color: white; - padding: 10px; -} - -#scko-welcome-message { - font-size: 16px; - background: none repeat scroll 0 0 #252525; - color: white; - padding: 10px; -} - -#scko-circ-info-div fieldset { - margin: 15px 10px 15px 10px; - padding: 8px; - background: #ffffff; /* Old browsers */ - /* IE9 SVG, needs conditional override of 'filter' to 'none' */ - background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9Ijk4JSIgc3RvcC1jb2xvcj0iI2UwZTBlMCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=); - background: -moz-linear-gradient(top, #ffffff 0%, #e0e0e0 98%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(98%,#e0e0e0)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #ffffff 0%,#e0e0e0 98%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #ffffff 0%,#e0e0e0 98%); /* Opera 11.10+ */ - background: -ms-linear-gradient(top, #ffffff 0%,#e0e0e0 98%); /* IE10+ */ - background: linear-gradient(to bottom, #ffffff 0%,#e0e0e0 98%); /* W3C */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#e0e0e0',GradientType=0 ); /* IE6-8 */ - border: 1px solid #DDDDDD; - color: #333333; - -webkit-border-radius: 5px; - border-radius: 5px; -} - -#scko-circ-info-div fieldset legend { - font-size: 13px; - color: #00593d; - background-color: white; - font-weight: bold; -} - -.scko-button { - color: #FBF9F9; - font-weight: bold; - letter-spacing: 1px; - font-size: 94%; - text-shadow: 1px 1px 1px #555555; - cursor: pointer !important; - border-radius: 12px; - border: 1px solid #007a54; - background: linear-gradient(#007a54, #00593d); - background: -moz-linear-gradient(#007a54, #00593d); - background: -o-linear-gradient(#007a54, #00593d); - background: -webkit-linear-gradient(#007a54, #00593d); - background-color: #00593d; - padding: 5px 10px 6px; - outline: 0 none; - text-decoration: none; -} - - -.oils-selfck-item-table { - width: 98%; - margin-top: 15px; -} - -.oils-selfck-item-table td { - text-align: left; - padding: 10px; -} - -.oils-selfck-item-table thead { - font-weight: bold; - color: #00593D; -} - -.oils-selfck-item-table tbody tr { - border-bottom: 1px solid #888; -} - -.scko-page { - margin-top: 20px; - background: #ffffff; /* Old browsers */ - /* IE9 SVG, needs conditional override of 'filter' to 'none' */ - background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9Ijk4JSIgc3RvcC1jb2xvcj0iI2UwZTBlMCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=); - background: -moz-linear-gradient(top, #ffffff 0%, #e0e0e0 98%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(98%,#e0e0e0)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #ffffff 0%,#e0e0e0 98%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #ffffff 0%,#e0e0e0 98%); /* Opera 11.10+ */ - background: -ms-linear-gradient(top, #ffffff 0%,#e0e0e0 98%); /* IE10+ */ - background: linear-gradient(to bottom, #ffffff 0%,#e0e0e0 98%); /* W3C */ - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#e0e0e0',GradientType=0 ); /* IE6-8 */ - border: 1px solid #DDDDDD; - color: #333333; - -webkit-border-radius: 5px; - border-radius: 5px; - padding: 3px; -} - -.scko-status-row { - font-size: 20px; -} diff --git a/Open-ILS/src/eg2/src/app/scko/scko.component.html b/Open-ILS/src/eg2/src/app/scko/scko.component.html deleted file mode 100644 index 1ae367dffe..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/scko.component.html +++ /dev/null @@ -1,108 +0,0 @@ - - - -
-
-
{{scko.statusDisplayText}}
-
-
- -
-
-
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - -No more renewals allowed for item {{barcode}} - - - - - - Item {{barcode}} was not found in the system. Try re-scanning the item. - - - - - - Item {{barcode}} is not allowed to circulate - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Open-ILS/src/eg2/src/app/scko/scko.component.ts b/Open-ILS/src/eg2/src/app/scko/scko.component.ts deleted file mode 100644 index 1cea153b4e..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/scko.component.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {Component, OnInit, AfterViewInit, ViewChild, ViewEncapsulation} from '@angular/core'; -import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; -import {AuthService} from '@eg/core/auth.service'; -import {NetService} from '@eg/core/net.service'; -import {SckoService} from './scko.service'; -import {ServerStoreService} from '@eg/core/server-store.service'; -import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; -import {AlertDialogComponent} from '@eg/share/dialog/alert.component'; - -@Component({ - templateUrl: 'scko.component.html', - styleUrls: ['scko.component.css'], - encapsulation: ViewEncapsulation.None -}) - -export class SckoComponent implements OnInit, AfterViewInit { - - @ViewChild('logoutDialog') logoutDialog: ConfirmDialogComponent; - @ViewChild('alertDialog') alertDialog: ConfirmDialogComponent; - - constructor( - private router: Router, - private route: ActivatedRoute, - private net: NetService, - private auth: AuthService, - public scko: SckoService - ) {} - - ngOnInit() { - this.net.authExpired$.subscribe(how => { - console.debug('SCKO auth expired with info', how); - this.scko.logoutStaff(); - }); - - this.scko.load(); - } - - ngAfterViewInit() { - this.scko.logoutDialog = this.logoutDialog; - this.scko.alertDialog = this.alertDialog; - } -} - diff --git a/Open-ILS/src/eg2/src/app/scko/scko.module.ts b/Open-ILS/src/eg2/src/app/scko/scko.module.ts deleted file mode 100644 index da4f188a14..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/scko.module.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {NgModule} from '@angular/core'; -import {EgCommonModule} from '@eg/common.module'; -import {CommonWidgetsModule} from '@eg/share/common-widgets.module'; -import {AudioService} from '@eg/share/util/audio.service'; -import {TitleComponent} from '@eg/share/title/title.component'; -import {PatronModule} from '@eg/staff/share/patron/patron.module'; - -import {SckoComponent} from './scko.component'; -import {SckoRoutingModule} from './routing.module'; -import {SckoService} from './scko.service'; -import {SckoBannerComponent} from './banner.component'; -import {SckoSummaryComponent} from './summary.component'; -import {SckoCheckoutComponent} from './checkout.component'; -import {SckoItemsComponent} from './items.component'; -import {SckoHoldsComponent} from './holds.component'; -import {SckoFinesComponent} from './fines.component'; - -@NgModule({ - declarations: [ - SckoComponent, - SckoBannerComponent, - SckoSummaryComponent, - SckoCheckoutComponent, - SckoItemsComponent, - SckoHoldsComponent, - SckoFinesComponent - ], - imports: [ - EgCommonModule, - CommonWidgetsModule, - PatronModule, - SckoRoutingModule - ], - providers: [ - SckoService, - AudioService - ] -}) - -export class SckoModule {} - diff --git a/Open-ILS/src/eg2/src/app/scko/scko.service.ts b/Open-ILS/src/eg2/src/app/scko/scko.service.ts deleted file mode 100644 index 501e094cde..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/scko.service.ts +++ /dev/null @@ -1,693 +0,0 @@ -import {Injectable, EventEmitter} from '@angular/core'; -import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; -import {Observable} from 'rxjs'; -import {OrgService} from '@eg/core/org.service'; -import {NetService} from '@eg/core/net.service'; -import {AuthService} from '@eg/core/auth.service'; -import {EventService, EgEvent} from '@eg/core/event.service'; -import {IdlService, IdlObject} from '@eg/core/idl.service'; -import {StoreService} from '@eg/core/store.service'; -import {PatronService, PatronSummary, PatronStats} from '@eg/staff/share/patron/patron.service'; -import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; -import {AlertDialogComponent} from '@eg/share/dialog/alert.component'; -import {PrintService} from '@eg/share/print/print.service'; -import {AudioService} from '@eg/share/util/audio.service'; -import {StringService} from '@eg/share/string/string.service'; -import {PcrudService} from '@eg/core/pcrud.service'; - -interface CheckoutContext { - barcode: string; // item - result: any; - firstEvent: EgEvent; - payload: any; - override: boolean; - redo: boolean; - renew: boolean; - displayText: string; // string key - alertSound: string; - shouldPopup: boolean; - previousCirc?: IdlObject; - renewalFailure?: boolean; -} - -interface SessionCheckout { - circ: IdlObject; - ctx: CheckoutContext; -} - -const CIRC_FLESH_DEPTH = 4; -const CIRC_FLESH_FIELDS = { - circ: ['target_copy'], - acp: ['call_number'], - acn: ['record'], - bre: ['flat_display_entries'] -}; - -@Injectable({providedIn: 'root'}) -export class SckoService { - - // Currently active patron account object. - patronSummary: PatronSummary; - statusDisplayText = ''; - statusDisplaySuccess: boolean; - - barcodeRegex: RegExp; - patronPasswordRequired = false; - patronIdleTimeout: number; - patronTimeoutId: number; - logoutWarningTimeout = 20; - logoutWarningTimerId: number; - - alertAudio = false; - alertPopup = false; - orgSettings: any; - overrideCheckoutEvents: string[] = []; - blockStatuses: number[] = []; - - sessionCheckouts: SessionCheckout[] = []; - - // We get this from the main scko component. - logoutDialog: ConfirmDialogComponent; - alertDialog: AlertDialogComponent; - focusBarcode: EventEmitter = new EventEmitter(); - patronLoaded: EventEmitter = new EventEmitter(); - - 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 { - 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 { - 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 { - return this.getFleshedCircs([circId]).toPromise(); - } - - loadPatron(username: string, password?: string): Promise { - 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 { - - 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 { - 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 { - - 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 { - - 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 { - - 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 { - 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 { - - 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 { - - 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 { - - 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 { - - return new Promise((resolve, reject) => { - - const sub = this.printer.printJobQueued$.subscribe(_ => { - sub.unsubscribe(); - // Give the print operation just a bit more time after - // the data is passed to the printer just to be safe. - setTimeout(() => resolve(null), 1000); - }); - - const data = this.sessionCheckouts.map(c => { - const circ = c.circ || c.ctx.previousCirc; - return { - checkout: c, - barcode: c.ctx.barcode, - circ: circ, - copy: circ ? circ.target_copy() : null, - title: this.getCircTitle(circ), - author: this.getCircAuthor(circ) - }; - }); - - this.printer.print({ - templateName: 'scko_checkouts', - contextData: { - checkouts: data, - user: this.patronSummary.patron - }, - printContext: 'default' - }); - }); - } - - copyIsPrecat(copy: IdlObject): boolean { - return Number(copy.id()) === -1; - } - - circDisplayValue(circ: IdlObject, field: string): string { - if (!circ) { return ''; } - - const entry = - circ.target_copy().call_number().record().flat_display_entries() - .filter(e => e.name() === field)[0]; - - return entry ? entry.value() : ''; - } - - getCircTitle(circ: IdlObject): string { - if (!circ) { return ''; } - const copy = circ.target_copy(); - if (this.copyIsPrecat(copy)) { return copy.dummy_title(); } - return this.circDisplayValue(circ, 'title'); - } - - getCircAuthor(circ: IdlObject): string { - if (!circ) { return ''; } - const copy = circ.target_copy(); - if (this.copyIsPrecat(copy)) { return copy.dummy_author(); } - return this.circDisplayValue(circ, 'author'); - } - -} - - - diff --git a/Open-ILS/src/eg2/src/app/scko/summary.component.html b/Open-ILS/src/eg2/src/app/scko/summary.component.html deleted file mode 100644 index f7329a04e3..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/summary.component.html +++ /dev/null @@ -1,91 +0,0 @@ - -
- - - -
- Receipt: - -
- - -
- -
- - -
- -
- - -
-
- -
- Items Checked Out -
- Total items this session: - {{scko.sessionTotalCheckouts()}} -
-
- Total items on account: - {{scko.accountTotalCheckouts()}} -
- -
-
- Holds -
- You have - {{scko.patronSummary.stats.holds.ready}} - item(s) ready for pickup. -
-
- You have - {{scko.patronSummary.stats.holds.total}} - total holds. -
- -
-
- Fines -
- Total fines on account: - - {{scko.patronSummary.stats.fines.balance_owed | currency}} - -
- -
-
- - diff --git a/Open-ILS/src/eg2/src/app/scko/summary.component.ts b/Open-ILS/src/eg2/src/app/scko/summary.component.ts deleted file mode 100644 index 454cabe782..0000000000 --- a/Open-ILS/src/eg2/src/app/scko/summary.component.ts +++ /dev/null @@ -1,53 +0,0 @@ -import {Component, OnInit, NgZone, HostListener} from '@angular/core'; -import {Location} from '@angular/common'; -import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; -import {AuthService, AuthWsState} from '@eg/core/auth.service'; -import {NetService} from '@eg/core/net.service'; -import {StoreService} from '@eg/core/store.service'; -import {SckoService} from './scko.service'; -import {OrgService} from '@eg/core/org.service'; -import {EventService, EgEvent} from '@eg/core/event.service'; - -@Component({ - selector: 'eg-scko-summary', - templateUrl: 'summary.component.html' -}) - -export class SckoSummaryComponent implements OnInit { - - showEmailOption = false; - receiptType = 'email'; - - constructor( - public scko: SckoService - ) {} - - ngOnInit() { - this.scko.patronLoaded.subscribe(() => { - if (this.canEmail()) { - this.showEmailOption = true; - this.receiptType = 'email'; - } else { - this.showEmailOption = false; - this.receiptType = 'print'; - } - }); - } - - canEmail(): boolean { - if (!this.scko.patronSummary) { return false; } - - const patron = this.scko.patronSummary.patron; - - const setting = patron.settings().filter( - s => s.name() === 'circ.send_email_checkout_receipts')[0]; - - return ( - Boolean(patron.email()) - && patron.email().match(/.*@.*/) !== null - && setting - && setting.value() === 'true' // JSON - ); - } -} - diff --git a/Open-ILS/src/eg2/src/app/staff/scko/banner.component.html b/Open-ILS/src/eg2/src/app/staff/scko/banner.component.html new file mode 100644 index 0000000000..6f72e126ba --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/banner.component.html @@ -0,0 +1,137 @@ + +
+
+ +
+
+ +
+ Please log in with your username or library barcode. +
+
+
+
+
+ + + + + + + + + + +
+
+
+
+
+ + +
Please enter an item barcode
+
+
+
+
+ + + + + +
+
+
+
+
Welcome, + {{scko.patronSummary.patron.pref_first_given_name() + || scko.patronSummary.patron.first_given_name()}} +
+
+
+
+ +
+
+ +
+ +
+
+

Self-Checkout Staff Login

+
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+ +
+
Login Failed
+
+
+
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/banner.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/banner.component.ts new file mode 100644 index 0000000000..11476d26f8 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/banner.component.ts @@ -0,0 +1,144 @@ +import {Component, OnInit, AfterViewInit, NgZone, HostListener} from '@angular/core'; +import {Location} from '@angular/common'; +import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; +import {AuthService, AuthWsState} from '@eg/core/auth.service'; +import {NetService} from '@eg/core/net.service'; +import {StoreService} from '@eg/core/store.service'; +import {SckoService} from './scko.service'; +import {OrgService} from '@eg/core/org.service'; +import {EventService, EgEvent} from '@eg/core/event.service'; +import {HatchService} from '@eg/core/hatch.service'; + +@Component({ + selector: 'eg-scko-banner', + templateUrl: 'banner.component.html' +}) + +export class SckoBannerComponent implements OnInit, AfterViewInit { + + workstations: any[]; + workstationNotFound = false; + + patronUsername: string; + patronPassword: string; + patronLoginFailed = false; + + staffUsername: string; + staffPassword: string; + staffWorkstation: string; + staffLoginFailed = false; + + itemBarcode: string; + + constructor( + private route: ActivatedRoute, + private store: StoreService, + private net: NetService, + private auth: AuthService, + private evt: EventService, + private ngLocation: Location, + private org: OrgService, + private hatch: HatchService, + public scko: SckoService + ) {} + + ngOnInit() { + + this.hatch.connect(); + + this.store.getWorkstations() + .then(wsList => { + this.workstations = wsList; + return this.store.getDefaultWorkstation(); + }).then(def => { + this.staffWorkstation = def; + this.applyWorkstation(); + }); + } + + ngAfterViewInit() { + if (this.auth.token()) { + this.focusNode('patron-username'); + } else { + this.focusNode('staff-username'); + } + + this.scko.focusBarcode.subscribe(_ => this.focusNode('item-barcode')); + } + + focusNode(id: string) { + setTimeout(() => { + const node = document.getElementById(id); + if (node) { (node as HTMLInputElement).select(); } + }); + } + + applyWorkstation() { + const wanted = this.route.snapshot.queryParamMap.get('workstation'); + if (!wanted) { return; } // use the default + + const exists = this.workstations.filter(w => w.name === wanted)[0]; + if (exists) { + this.staffWorkstation = wanted; + } else { + console.error(`Unknown workstation requested: ${wanted}`); + } + } + + submitStaffLogin() { + + this.staffLoginFailed = false; + + const args = { + type: 'persistent', + username: this.staffUsername, + password: this.staffPassword, + workstation: this.staffWorkstation + }; + + this.staffLoginFailed = false; + this.workstationNotFound = false; + + this.auth.login(args).then( + ok => { + + if (this.auth.workstationState === AuthWsState.NOT_FOUND_SERVER) { + this.staffLoginFailed = true; + this.workstationNotFound = true; + + } else { + + // Initial login clears cached org unit setting values + // and user/workstation setting values + this.org.clearCachedSettings().then(_ => { + + // Force reload of the app after a successful login. + window.location.href = + this.ngLocation.prepareExternalUrl('/staff/scko'); + + }); + } + }, + notOk => { + this.staffLoginFailed = true; + } + ); + } + + submitPatronLogin() { + this.patronLoginFailed = false; + this.scko.loadPatron(this.patronUsername, this.patronPassword).finally(() => { + if (this.scko.patronSummary === null) { + this.patronLoginFailed = true; + } else { + this.focusNode('item-barcode'); + } + }); + } + + submitItemBarcode() { + this.scko.resetPatronTimeout(); + this.scko.checkout(this.itemBarcode); + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/scko/checkout.component.html b/Open-ILS/src/eg2/src/app/staff/scko/checkout.component.html new file mode 100644 index 0000000000..51f549f5a4 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/checkout.component.html @@ -0,0 +1,35 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + +
BarcodeTitleAuthorDue DateRenewals LeftType
+ + + + {{co.circ.target_copy().barcode()}}{{scko.getCircTitle(co.circ)}}{{scko.getCircAuthor(co.circ)}}{{co.circ | egDueDate}}{{co.circ.renewal_remaining()}} + + Renewal + Checkout + +
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/checkout.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/checkout.component.ts new file mode 100644 index 0000000000..5afc1f6f29 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/checkout.component.ts @@ -0,0 +1,23 @@ +import {Component, OnInit, ViewEncapsulation} from '@angular/core'; +import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; +import {AuthService} from '@eg/core/auth.service'; +import {IdlObject} from '@eg/core/idl.service'; +import {SckoService} from './scko.service'; +import {ServerStoreService} from '@eg/core/server-store.service'; + +@Component({ + templateUrl: 'checkout.component.html' +}) + +export class SckoCheckoutComponent implements OnInit { + + constructor( + private router: Router, + private route: ActivatedRoute, + public scko: SckoService + ) {} + + ngOnInit() { + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/scko/fines.component.html b/Open-ILS/src/eg2/src/app/staff/scko/fines.component.html new file mode 100644 index 0000000000..97f7464eee --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/fines.component.html @@ -0,0 +1,35 @@ +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + +
TypeDetailsTotal BilledTotal PaidBalance Owed
+ + Circulation + + + Miscellaneous + + {{getDetails(xact)}}{{xact.summary().total_owed() | currency}}{{xact.summary().total_paid() | currency}}{{xact.summary().balance_owed() | currency}}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/fines.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/fines.component.ts new file mode 100644 index 0000000000..220961475d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/fines.component.ts @@ -0,0 +1,110 @@ +import {Component, OnInit, ViewEncapsulation} from '@angular/core'; +import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; +import {switchMap, tap} from 'rxjs/operators'; +import {AuthService} from '@eg/core/auth.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {NetService} from '@eg/core/net.service'; +import {IdlObject} from '@eg/core/idl.service'; +import {SckoService} from './scko.service'; +import {PrintService} from '@eg/share/print/print.service'; + + +@Component({ + templateUrl: 'fines.component.html' +}) + +export class SckoFinesComponent implements OnInit { + + xacts: IdlObject[] = []; + + constructor( + private router: Router, + private route: ActivatedRoute, + private net: NetService, + private auth: AuthService, + private pcrud: PcrudService, + private printer: PrintService, + public scko: SckoService + ) {} + + ngOnInit() { + + if (!this.scko.patronSummary) { + this.router.navigate(['/staff/scko']); + return; + } + + this.scko.resetPatronTimeout(); + + this.pcrud.search('mbts', + { usr: this.scko.patronSummary.id, + xact_finish: null, + balance_owed: {'<>' : 0} + }, {}, {atomic: true} + ).pipe(switchMap(sums => { + + return this.pcrud.search('mbt', {id: sums.map(s => s.id())}, + { order_by: 'xact_start', + flesh: 5, + flesh_fields: { + mbt: ['summary', 'circulation', 'grocery'], + circ: ['target_copy'], + acp: ['call_number'], + acn: ['record'], + bre: ['flat_display_entries'] + }, + select: {bre : ['id']} + } + ).pipe(tap(xact => this.xacts.push(xact))); + })).toPromise(); + } + + displayValue(xact: IdlObject, field: string): string { + const entry = + xact.circulation().target_copy().call_number().record().flat_display_entries() + .filter(e => e.name() === field)[0]; + + return entry ? entry.value() : ''; + } + + getTitle(xact: IdlObject): string { + const copy = xact.circulation().target_copy(); + + if (Number(copy.call_number().id()) === -1) { + return copy.dummy_title(); + } + + return this.displayValue(xact, 'title'); + } + + getDetails(xact: IdlObject): string { + if (xact.summary().xact_type() === 'circulation') { + return this.getTitle(xact); + } else { + return xact.summary().last_billing_type(); + } + } + + printList() { + + const data = this.xacts.map(x => { + return { + xact: x, // full object if needed + details: this.getDetails(x), + total_owed: x.summary().total_owed(), + total_paid: x.summary().total_paid(), + balance_owed: x.summary().balance_owed(), + }; + }); + + this.printer.print({ + templateName: 'scko_fines', + contextData: { + xacts: data, + user: this.scko.patronSummary.patron + }, + printContext: 'default' + }); + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/scko/holds.component.html b/Open-ILS/src/eg2/src/app/staff/scko/holds.component.html new file mode 100644 index 0000000000..09e3b259eb --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/holds.component.html @@ -0,0 +1,33 @@ +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + +
TitleAuthorStatus
{{hold.title}}{{hold.author}} + + Ready for Pickup + + + #{{hold.relative_queue_position}} in line with {{hold.potentials}} copie(s) + +
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/holds.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/holds.component.ts new file mode 100644 index 0000000000..eddc802c9f --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/holds.component.ts @@ -0,0 +1,78 @@ +import {Component, OnInit, ViewEncapsulation} from '@angular/core'; +import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; +import {tap} from 'rxjs/operators'; +import {AuthService} from '@eg/core/auth.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {NetService} from '@eg/core/net.service'; +import {IdlObject} from '@eg/core/idl.service'; +import {SckoService} from './scko.service'; +import {ServerStoreService} from '@eg/core/server-store.service'; +import {PrintService} from '@eg/share/print/print.service'; + +@Component({ + templateUrl: 'holds.component.html' +}) + +export class SckoHoldsComponent implements OnInit { + + holds: IdlObject[] = []; + + constructor( + private router: Router, + private route: ActivatedRoute, + private net: NetService, + private auth: AuthService, + private pcrud: PcrudService, + private printer: PrintService, + public scko: SckoService + ) {} + + ngOnInit() { + + if (!this.scko.patronSummary) { + this.router.navigate(['/staff/scko']); + return; + } + + this.scko.resetPatronTimeout(); + + const orderBy = [ + {shelf_time: {nulls: 'last'}}, + {capture_time: {nulls: 'last'}}, + {request_time: {nulls: 'last'}} + ]; + + const filters = { + usr_id: this.scko.patronSummary.id, + fulfillment_time: null + }; + + let first = true; + this.net.request( + 'open-ils.circ', + 'open-ils.circ.hold.wide_hash.stream', + this.auth.token(), filters, orderBy, 1000, 0, {} + ).subscribe(holdData => { + + if (first) { // First response is the hold count. + first = false; + return; + } + + this.holds.push(holdData); + }); + } + + printList() { + this.printer.print({ + templateName: 'scko_holds', + contextData: { + holds: this.holds, + user: this.scko.patronSummary.patron + }, + printContext: 'default' + }); + } +} + + diff --git a/Open-ILS/src/eg2/src/app/staff/scko/items.component.html b/Open-ILS/src/eg2/src/app/staff/scko/items.component.html new file mode 100644 index 0000000000..63e36db4bd --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/items.component.html @@ -0,0 +1,39 @@ +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
BarcodeTitleAuthorDue DateRenewals LeftType
+ + + + {{circ.target_copy().barcode()}}{{scko.getCircTitle(circ)}}{{scko.getCircAuthor(circ)}}{{circ | egDueDate}}{{circ.renewal_remaining()}} + Renewal + Checkout +
+
diff --git a/Open-ILS/src/eg2/src/app/staff/scko/items.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/items.component.ts new file mode 100644 index 0000000000..2926a27866 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/items.component.ts @@ -0,0 +1,75 @@ +import {Component, OnInit, ViewEncapsulation} from '@angular/core'; +import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; +import {tap} from 'rxjs/operators'; +import {AuthService} from '@eg/core/auth.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {NetService} from '@eg/core/net.service'; +import {IdlObject} from '@eg/core/idl.service'; +import {SckoService} from './scko.service'; +import {ServerStoreService} from '@eg/core/server-store.service'; +import {PrintService} from '@eg/share/print/print.service'; + +@Component({ + templateUrl: 'items.component.html' +}) + +export class SckoItemsComponent implements OnInit { + + circs: IdlObject[] = []; + + constructor( + private router: Router, + private route: ActivatedRoute, + private net: NetService, + private auth: AuthService, + private pcrud: PcrudService, + private printer: PrintService, + public scko: SckoService + ) {} + + ngOnInit() { + + if (!this.scko.patronSummary) { + this.router.navigate(['/staff/scko']); + return; + } + + this.scko.resetPatronTimeout(); + + this.net.request( + 'open-ils.actor', + 'open-ils.actor.user.checked_out.authoritative', + this.auth.token(), this.scko.patronSummary.id).toPromise() + + .then(data => { + const ids = data.out.concat(data.overdue).concat(data.long_overdue); + return this.scko.getFleshedCircs(ids).pipe(tap(circ => { + this.circs.push(circ); + })).toPromise(); + }); + } + + printList() { + + const data = this.circs.map(c => { + return { + circ: c, + copy: c.target_copy(), + title: this.scko.getCircTitle(c), + author: this.scko.getCircAuthor(c) + }; + }); + + this.printer.print({ + templateName: 'scko_items_out', + contextData: { + checkouts: data, + user: this.scko.patronSummary.patron + }, + printContext: 'default' + }); + } +} + + + diff --git a/Open-ILS/src/eg2/src/app/staff/scko/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/scko/routing.module.ts new file mode 100644 index 0000000000..08814530a1 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/routing.module.ts @@ -0,0 +1,33 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {SckoComponent} from './scko.component'; +import {SckoCheckoutComponent} from './checkout.component'; +import {SckoItemsComponent} from './items.component'; +import {SckoHoldsComponent} from './holds.component'; +import {SckoFinesComponent} from './fines.component'; + +const routes: Routes = [{ + path: '', + component: SckoComponent, + children: [{ + path: '', + component: SckoCheckoutComponent + }, { + path: 'items', + component: SckoItemsComponent + }, { + path: 'holds', + component: SckoHoldsComponent + }, { + path: 'fines', + component: SckoFinesComponent + }] +}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) + +export class SckoRoutingModule {} + diff --git a/Open-ILS/src/eg2/src/app/staff/scko/scko.component.css b/Open-ILS/src/eg2/src/app/staff/scko/scko.component.css new file mode 100644 index 0000000000..0150ee34a1 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/scko.component.css @@ -0,0 +1,126 @@ +body { + font-family: Arial, Verdana; + font-size: 13px; +} + +A { + text-decoration: none; +} + +#scko-banner { + background: #00593d; /* Old browsers */ +/* IE9 SVG, needs conditional override of 'filter' to 'none' */ + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwNTkzZCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMwMDdhNTQiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+); + background: -moz-linear-gradient(top, #00593d 0%, #007a54 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00593d), color-stop(100%,#007a54)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #00593d 0%,#007a54 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #00593d 0%,#007a54 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #00593d 0%,#007a54 100%); /* IE10+ */ + background: linear-gradient(to bottom, #00593d 0%,#007a54 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00593d', endColorstr='#007a54',GradientType=0 ); /* IE6-8 */ + padding-top: 20px; + text-align: center; + font-weight:bold; +} + +#scko-scan-input-text { + font-size: 16px; + background: none repeat scroll 0 0 #252525; + color: white; + padding: 10px; +} + +#scko-welcome-message { + font-size: 16px; + background: none repeat scroll 0 0 #252525; + color: white; + padding: 10px; +} + +#scko-circ-info-div fieldset { + margin: 15px 10px 15px 10px; + padding: 8px; + background: #ffffff; /* Old browsers */ + /* IE9 SVG, needs conditional override of 'filter' to 'none' */ + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9Ijk4JSIgc3RvcC1jb2xvcj0iI2UwZTBlMCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=); + background: -moz-linear-gradient(top, #ffffff 0%, #e0e0e0 98%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(98%,#e0e0e0)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ffffff 0%,#e0e0e0 98%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ffffff 0%,#e0e0e0 98%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #ffffff 0%,#e0e0e0 98%); /* IE10+ */ + background: linear-gradient(to bottom, #ffffff 0%,#e0e0e0 98%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#e0e0e0',GradientType=0 ); /* IE6-8 */ + border: 1px solid #DDDDDD; + color: #333333; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +#scko-circ-info-div fieldset legend { + font-size: 13px; + color: #00593d; + background-color: white; + font-weight: bold; +} + +.scko-button { + color: #FBF9F9; + font-weight: bold; + letter-spacing: 1px; + font-size: 94%; + text-shadow: 1px 1px 1px #555555; + cursor: pointer !important; + border-radius: 12px; + border: 1px solid #007a54; + background: linear-gradient(#007a54, #00593d); + background: -moz-linear-gradient(#007a54, #00593d); + background: -o-linear-gradient(#007a54, #00593d); + background: -webkit-linear-gradient(#007a54, #00593d); + background-color: #00593d; + padding: 5px 10px 6px; + outline: 0 none; + text-decoration: none; +} + + +.oils-selfck-item-table { + width: 98%; + margin-top: 15px; +} + +.oils-selfck-item-table td { + text-align: left; + padding: 10px; +} + +.oils-selfck-item-table thead { + font-weight: bold; + color: #00593D; +} + +.oils-selfck-item-table tbody tr { + border-bottom: 1px solid #888; +} + +.scko-page { + margin-top: 20px; + background: #ffffff; /* Old browsers */ + /* IE9 SVG, needs conditional override of 'filter' to 'none' */ + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9Ijk4JSIgc3RvcC1jb2xvcj0iI2UwZTBlMCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgPC9saW5lYXJHcmFkaWVudD4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdWNnZy1nZW5lcmF0ZWQpIiAvPgo8L3N2Zz4=); + background: -moz-linear-gradient(top, #ffffff 0%, #e0e0e0 98%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(98%,#e0e0e0)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ffffff 0%,#e0e0e0 98%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ffffff 0%,#e0e0e0 98%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #ffffff 0%,#e0e0e0 98%); /* IE10+ */ + background: linear-gradient(to bottom, #ffffff 0%,#e0e0e0 98%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#e0e0e0',GradientType=0 ); /* IE6-8 */ + border: 1px solid #DDDDDD; + color: #333333; + -webkit-border-radius: 5px; + border-radius: 5px; + padding: 3px; +} + +.scko-status-row { + font-size: 20px; +} diff --git a/Open-ILS/src/eg2/src/app/staff/scko/scko.component.html b/Open-ILS/src/eg2/src/app/staff/scko/scko.component.html new file mode 100644 index 0000000000..1ae367dffe --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/scko.component.html @@ -0,0 +1,108 @@ + + + +
+
+
{{scko.statusDisplayText}}
+
+
+ +
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +No more renewals allowed for item {{barcode}} + + + + + + Item {{barcode}} was not found in the system. Try re-scanning the item. + + + + + + Item {{barcode}} is not allowed to circulate + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/scko/scko.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/scko.component.ts new file mode 100644 index 0000000000..1cea153b4e --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/scko.component.ts @@ -0,0 +1,43 @@ +import {Component, OnInit, AfterViewInit, ViewChild, ViewEncapsulation} from '@angular/core'; +import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; +import {AuthService} from '@eg/core/auth.service'; +import {NetService} from '@eg/core/net.service'; +import {SckoService} from './scko.service'; +import {ServerStoreService} from '@eg/core/server-store.service'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {AlertDialogComponent} from '@eg/share/dialog/alert.component'; + +@Component({ + templateUrl: 'scko.component.html', + styleUrls: ['scko.component.css'], + encapsulation: ViewEncapsulation.None +}) + +export class SckoComponent implements OnInit, AfterViewInit { + + @ViewChild('logoutDialog') logoutDialog: ConfirmDialogComponent; + @ViewChild('alertDialog') alertDialog: ConfirmDialogComponent; + + constructor( + private router: Router, + private route: ActivatedRoute, + private net: NetService, + private auth: AuthService, + public scko: SckoService + ) {} + + ngOnInit() { + this.net.authExpired$.subscribe(how => { + console.debug('SCKO auth expired with info', how); + this.scko.logoutStaff(); + }); + + this.scko.load(); + } + + ngAfterViewInit() { + this.scko.logoutDialog = this.logoutDialog; + this.scko.alertDialog = this.alertDialog; + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/scko/scko.module.ts b/Open-ILS/src/eg2/src/app/staff/scko/scko.module.ts new file mode 100644 index 0000000000..da4f188a14 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/scko.module.ts @@ -0,0 +1,41 @@ +import {NgModule} from '@angular/core'; +import {EgCommonModule} from '@eg/common.module'; +import {CommonWidgetsModule} from '@eg/share/common-widgets.module'; +import {AudioService} from '@eg/share/util/audio.service'; +import {TitleComponent} from '@eg/share/title/title.component'; +import {PatronModule} from '@eg/staff/share/patron/patron.module'; + +import {SckoComponent} from './scko.component'; +import {SckoRoutingModule} from './routing.module'; +import {SckoService} from './scko.service'; +import {SckoBannerComponent} from './banner.component'; +import {SckoSummaryComponent} from './summary.component'; +import {SckoCheckoutComponent} from './checkout.component'; +import {SckoItemsComponent} from './items.component'; +import {SckoHoldsComponent} from './holds.component'; +import {SckoFinesComponent} from './fines.component'; + +@NgModule({ + declarations: [ + SckoComponent, + SckoBannerComponent, + SckoSummaryComponent, + SckoCheckoutComponent, + SckoItemsComponent, + SckoHoldsComponent, + SckoFinesComponent + ], + imports: [ + EgCommonModule, + CommonWidgetsModule, + PatronModule, + SckoRoutingModule + ], + providers: [ + SckoService, + AudioService + ] +}) + +export class SckoModule {} + diff --git a/Open-ILS/src/eg2/src/app/staff/scko/scko.service.ts b/Open-ILS/src/eg2/src/app/staff/scko/scko.service.ts new file mode 100644 index 0000000000..1398fa9892 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/scko.service.ts @@ -0,0 +1,693 @@ +import {Injectable, EventEmitter} from '@angular/core'; +import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; +import {Observable} from 'rxjs'; +import {OrgService} from '@eg/core/org.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {EventService, EgEvent} from '@eg/core/event.service'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {StoreService} from '@eg/core/store.service'; +import {PatronService, PatronSummary, PatronStats} from '@eg/staff/share/patron/patron.service'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {AlertDialogComponent} from '@eg/share/dialog/alert.component'; +import {PrintService} from '@eg/share/print/print.service'; +import {AudioService} from '@eg/share/util/audio.service'; +import {StringService} from '@eg/share/string/string.service'; +import {PcrudService} from '@eg/core/pcrud.service'; + +interface CheckoutContext { + barcode: string; // item + result: any; + firstEvent: EgEvent; + payload: any; + override: boolean; + redo: boolean; + renew: boolean; + displayText: string; // string key + alertSound: string; + shouldPopup: boolean; + previousCirc?: IdlObject; + renewalFailure?: boolean; +} + +interface SessionCheckout { + circ: IdlObject; + ctx: CheckoutContext; +} + +const CIRC_FLESH_DEPTH = 4; +const CIRC_FLESH_FIELDS = { + circ: ['target_copy'], + acp: ['call_number'], + acn: ['record'], + bre: ['flat_display_entries'] +}; + +@Injectable({providedIn: 'root'}) +export class SckoService { + + // Currently active patron account object. + patronSummary: PatronSummary; + statusDisplayText = ''; + statusDisplaySuccess: boolean; + + barcodeRegex: RegExp; + patronPasswordRequired = false; + patronIdleTimeout: number; + patronTimeoutId: number; + logoutWarningTimeout = 20; + logoutWarningTimerId: number; + + alertAudio = false; + alertPopup = false; + orgSettings: any; + overrideCheckoutEvents: string[] = []; + blockStatuses: number[] = []; + + sessionCheckouts: SessionCheckout[] = []; + + // We get this from the main scko component. + logoutDialog: ConfirmDialogComponent; + alertDialog: AlertDialogComponent; + focusBarcode: EventEmitter = new EventEmitter(); + patronLoaded: EventEmitter = new EventEmitter(); + + 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 { + 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 { + 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 { + return this.getFleshedCircs([circId]).toPromise(); + } + + loadPatron(username: string, password?: string): Promise { + 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 { + + 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 { + 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 { + + 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 { + + 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 { + + 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 { + 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 { + + 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 { + + 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 { + + 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 { + + return new Promise((resolve, reject) => { + + const sub = this.printer.printJobQueued$.subscribe(_ => { + sub.unsubscribe(); + // Give the print operation just a bit more time after + // the data is passed to the printer just to be safe. + setTimeout(() => resolve(null), 1000); + }); + + const data = this.sessionCheckouts.map(c => { + const circ = c.circ || c.ctx.previousCirc; + return { + checkout: c, + barcode: c.ctx.barcode, + circ: circ, + copy: circ ? circ.target_copy() : null, + title: this.getCircTitle(circ), + author: this.getCircAuthor(circ) + }; + }); + + this.printer.print({ + templateName: 'scko_checkouts', + contextData: { + checkouts: data, + user: this.patronSummary.patron + }, + printContext: 'default' + }); + }); + } + + copyIsPrecat(copy: IdlObject): boolean { + return Number(copy.id()) === -1; + } + + circDisplayValue(circ: IdlObject, field: string): string { + if (!circ) { return ''; } + + const entry = + circ.target_copy().call_number().record().flat_display_entries() + .filter(e => e.name() === field)[0]; + + return entry ? entry.value() : ''; + } + + getCircTitle(circ: IdlObject): string { + if (!circ) { return ''; } + const copy = circ.target_copy(); + if (this.copyIsPrecat(copy)) { return copy.dummy_title(); } + return this.circDisplayValue(circ, 'title'); + } + + getCircAuthor(circ: IdlObject): string { + if (!circ) { return ''; } + const copy = circ.target_copy(); + if (this.copyIsPrecat(copy)) { return copy.dummy_author(); } + return this.circDisplayValue(circ, 'author'); + } + +} + + + diff --git a/Open-ILS/src/eg2/src/app/staff/scko/summary.component.html b/Open-ILS/src/eg2/src/app/staff/scko/summary.component.html new file mode 100644 index 0000000000..7ca92bb327 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/summary.component.html @@ -0,0 +1,91 @@ + +
+ + + +
+ Receipt: + +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ Items Checked Out +
+ Total items this session: + {{scko.sessionTotalCheckouts()}} +
+
+ Total items on account: + {{scko.accountTotalCheckouts()}} +
+ +
+
+ Holds +
+ You have + {{scko.patronSummary.stats.holds.ready}} + item(s) ready for pickup. +
+
+ You have + {{scko.patronSummary.stats.holds.total}} + total holds. +
+ +
+
+ Fines +
+ Total fines on account: + + {{scko.patronSummary.stats.fines.balance_owed | currency}} + +
+ +
+
+ + diff --git a/Open-ILS/src/eg2/src/app/staff/scko/summary.component.ts b/Open-ILS/src/eg2/src/app/staff/scko/summary.component.ts new file mode 100644 index 0000000000..454cabe782 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/scko/summary.component.ts @@ -0,0 +1,53 @@ +import {Component, OnInit, NgZone, HostListener} from '@angular/core'; +import {Location} from '@angular/common'; +import {Router, ActivatedRoute, NavigationEnd} from '@angular/router'; +import {AuthService, AuthWsState} from '@eg/core/auth.service'; +import {NetService} from '@eg/core/net.service'; +import {StoreService} from '@eg/core/store.service'; +import {SckoService} from './scko.service'; +import {OrgService} from '@eg/core/org.service'; +import {EventService, EgEvent} from '@eg/core/event.service'; + +@Component({ + selector: 'eg-scko-summary', + templateUrl: 'summary.component.html' +}) + +export class SckoSummaryComponent implements OnInit { + + showEmailOption = false; + receiptType = 'email'; + + constructor( + public scko: SckoService + ) {} + + ngOnInit() { + this.scko.patronLoaded.subscribe(() => { + if (this.canEmail()) { + this.showEmailOption = true; + this.receiptType = 'email'; + } else { + this.showEmailOption = false; + this.receiptType = 'print'; + } + }); + } + + canEmail(): boolean { + if (!this.scko.patronSummary) { return false; } + + const patron = this.scko.patronSummary.patron; + + const setting = patron.settings().filter( + s => s.name() === 'circ.send_email_checkout_receipts')[0]; + + return ( + Boolean(patron.email()) + && patron.email().match(/.*@.*/) !== null + && setting + && setting.value() === 'true' // JSON + ); + } +} +