From e25c00465ea2021ca4b3260f09ff34958331732c Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Fri, 9 Apr 2021 15:38:57 -0400 Subject: [PATCH] LP1904036 checkin UI Signed-off-by: Bill Erickson Signed-off-by: Jane Sandberg Signed-off-by: Galen Charlton --- .../app/staff/circ/checkin/checkin.component.html | 107 ++++++++++++++ .../app/staff/circ/checkin/checkin.component.ts | 156 +++++++++++++++++++++ .../src/app/staff/circ/checkin/checkin.module.ts | 35 +++++ .../src/app/staff/circ/checkin/routing.module.ts | 15 ++ .../src/eg2/src/app/staff/circ/routing.module.ts | 4 + Open-ILS/src/eg2/src/app/staff/nav.component.html | 6 + .../eg2/src/app/staff/share/circ/circ.service.ts | 49 ++++++- .../app/staff/share/circ/route-dialog.component.ts | 2 - 8 files changed, 367 insertions(+), 7 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/checkin/routing.module.ts diff --git a/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.component.html b/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.component.html new file mode 100644 index 0000000000..0ad28c6787 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.component.html @@ -0,0 +1,107 @@ + + + + + + +
+
+
+
+
+ Barcode +
+ +
+ +
+
+
+
+
+ Effective Date: + +
+
+
+ +
+ Fine Tally: + {{fineTally | currency}} +
+ + + + + {{r.title}} + + {{r.title}} + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ diff --git a/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.component.ts new file mode 100644 index 0000000000..6ad1c8f679 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.component.ts @@ -0,0 +1,156 @@ +import {Component, ViewChild, OnInit, AfterViewInit, HostListener} from '@angular/core'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {from} from 'rxjs'; +import {NetService} from '@eg/core/net.service'; +import {OrgService} from '@eg/core/org.service'; +import {AuthService} from '@eg/core/auth.service'; +import {ServerStoreService} from '@eg/core/server-store.service'; +import {PatronService} from '@eg/staff/share/patron/patron.service'; +import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {CircService, CircDisplayInfo, CheckinParams, CheckinResult + } from '@eg/staff/share/circ/circ.service'; +import {Pager} from '@eg/share/util/pager'; +import {BarcodeSelectComponent + } from '@eg/staff/share/barcodes/barcode-select.component'; + +interface CheckinGridEntry extends CheckinResult { + title?: string; + author?: string; + isbn?: string; +} + +@Component({ + templateUrl: 'checkin.component.html' +}) +export class CheckinComponent implements OnInit, AfterViewInit { + checkins: CheckinGridEntry[] = []; + autoIndex = 0; + + barcode: string; + backdate: string; // ISO + fineTally = 0; + + gridDataSource: GridDataSource = new GridDataSource(); + cellTextGenerator: GridCellTextGenerator; + + private copiesInFlight: {[barcode: string]: boolean} = {}; + + @ViewChild('grid') private grid: GridComponent; + @ViewChild('barcodeSelect') private barcodeSelect: BarcodeSelectComponent; + + constructor( + private router: Router, + private route: ActivatedRoute, + private net: NetService, + private org: OrgService, + private auth: AuthService, + private store: ServerStoreService, + private circ: CircService, + public patronService: PatronService + ) {} + + ngOnInit() { + this.gridDataSource.getRows = (pager: Pager, sort: any[]) => { + return from(this.checkins); + }; + } + + ngAfterViewInit() { + this.focusInput(); + } + + focusInput() { + const input = document.getElementById('barcode-input'); + if (input) { input.focus(); } + } + + checkin(params?: CheckinParams, override?: boolean): Promise { + if (!this.barcode) { return Promise.resolve(null); } + + const promise = params ? Promise.resolve(params) : this.collectParams(); + + return promise.then((collectedParams: CheckinParams) => { + if (!collectedParams) { return null; } + + if (this.copiesInFlight[this.barcode]) { + console.debug('Item ' + this.barcode + ' is already mid-checkin'); + return null; + } + + this.copiesInFlight[this.barcode] = true; + return this.circ.checkin(collectedParams); + }) + + .then((result: CheckinResult) => { + if (result) { + this.dispatchResult(result); + return result; + } + }) + + .finally(() => delete this.copiesInFlight[this.barcode]); + } + + dispatchResult(result: CheckinResult) { + if (result.success) { + this.gridifyResult(result); + this.resetForm(); + return; + } + } + + collectParams(): Promise { + + const params: CheckinParams = { + copy_barcode: this.barcode, + backdate: this.backdate + }; + + return this.barcodeSelect.getBarcode('asset', this.barcode) + .then(selection => { + if (selection) { + params.copy_id = selection.id; + params.copy_barcode = selection.barcode; + return params; + } else { + // User canceled the multi-match selection dialog. + return null; + } + }); + } + + resetForm() { + this.barcode = ''; + this.focusInput(); + } + + gridifyResult(result: CheckinResult) { + const entry: CheckinGridEntry = result; + entry.index = this.autoIndex++; + + if (result.record) { + entry.title = result.record.title(); + entry.author = result.record.author(); + entry.isbn = result.record.isbn(); + + } else if (result.copy) { + entry.title = result.copy.dummy_title(); + entry.author = result.copy.dummy_author(); + entry.isbn = result.copy.dummy_isbn(); + } + + if (result.copy) { + result.copy.circ_lib(this.org.get(result.copy.circ_lib())); + } + + if (result.mbts) { + this.fineTally = + ((this.fineTally * 100) + (result.mbts.balance_owed() * 100)) / 100; + } + + this.checkins.unshift(entry); + this.grid.reload(); + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.module.ts new file mode 100644 index 0000000000..2455fa0209 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.module.ts @@ -0,0 +1,35 @@ +import {NgModule} from '@angular/core'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {CheckinRoutingModule} from './routing.module'; +import {FmRecordEditorModule} from '@eg/share/fm-editor/fm-editor.module'; +import {HoldsModule} from '@eg/staff/share/holds/holds.module'; +import {BillingModule} from '@eg/staff/share/billing/billing.module'; +import {CircModule} from '@eg/staff/share/circ/circ.module'; +import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module'; +import {BookingModule} from '@eg/staff/share/booking/booking.module'; +import {PatronModule} from '@eg/staff/share/patron/patron.module'; +import {BarcodesModule} from '@eg/staff/share/barcodes/barcodes.module'; +import {CheckinComponent} from './checkin.component'; + +@NgModule({ + declarations: [ + CheckinComponent + ], + imports: [ + StaffCommonModule, + CheckinRoutingModule, + FmRecordEditorModule, + BillingModule, + CircModule, + HoldsModule, + HoldingsModule, + BookingModule, + PatronModule, + BarcodesModule + ], + providers: [ + ] +}) + +export class CheckinModule {} + diff --git a/Open-ILS/src/eg2/src/app/staff/circ/checkin/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/checkin/routing.module.ts new file mode 100644 index 0000000000..df175a0e22 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/checkin/routing.module.ts @@ -0,0 +1,15 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {CheckinComponent} from './checkin.component'; + +const routes: Routes = [{ + path: '', + component: CheckinComponent +}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) + +export class CheckinRoutingModule {} diff --git a/Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts index 54e0ae4a99..5122ec2ffa 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts @@ -13,6 +13,10 @@ const routes: Routes = [{ path: 'holds', loadChildren: () => import('./holds/holds.module').then(m => m.HoldsUiModule) +}, { + path: 'checkin', + loadChildren: () => + import('./checkin/checkin.module').then(m => m.CheckinModule) }]; @NgModule({ diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.html b/Open-ILS/src/eg2/src/app/staff/nav.component.html index 24b027f5c0..ec48cbcec8 100644 --- a/Open-ILS/src/eg2/src/app/staff/nav.component.html +++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html @@ -59,6 +59,12 @@ Check In + + + Check In (Experimental) + diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts index 619e31f812..c8a31c1d4e 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts @@ -149,6 +149,7 @@ export interface CheckinParams { claims_never_checked_out?: boolean; void_overdues?: boolean; auto_print_hold_transits?: boolean; + backdate?: string; // internal tracking _override?: boolean; @@ -163,11 +164,14 @@ export interface CheckinResult { copy?: IdlObject; volume?: IdlObject; circ?: IdlObject; + parent_circ?: IdlObject; + mbts?: IdlObject; record?: IdlObject; hold?: IdlObject; transit?: IdlObject; org?: number; patron?: IdlObject; + routeTo?: string; } @Injectable() @@ -206,10 +210,13 @@ export class CircService { // 'circ' is fleshed with copy, vol, bib, wide_display_entry // Extracts some display info from a fleshed circ. getDisplayInfo(circ: IdlObject): CircDisplayInfo { + return this.getCopyDisplayInfo(circ.target_copy()); + } - const copy = circ.target_copy(); + getCopyDisplayInfo(copy: IdlObject): CircDisplayInfo { - if (copy.call_number().id() === -1) { // precat + if (copy.call_number() === -1 || copy.call_number().id() === -1) { + // Precat Copy return { title: copy.dummy_title(), author: copy.dummy_author(), @@ -273,6 +280,7 @@ export class CircService { ).toPromise().then(transit => { transit.source(this.org.get(transit.source())); transit.dest(this.org.get(transit.dest())); + result.routeTo = transit.dest().shortname(); return transit; }); } @@ -541,15 +549,20 @@ export class CircService { params: params, success: success, circ: payload.circ, + parent_circ: payload.parent_circ, copy: payload.copy, volume: payload.volume, record: payload.record, transit: payload.transit }; - let promise = Promise.resolve();; const copy = result.copy; const volume = result.volume; + const transit = result.transit; + const circ = result.circ; + const parent_circ = result.parent_circ; + + let promise = Promise.resolve();; if (copy) { if (this.copyLocationCache[copy.location()]) { @@ -579,6 +592,25 @@ export class CircService { } } + if (transit) { + if (typeof transit.dest() !== 'object') { + transit.dest(this.org.get(transit.dest())); + } + if (typeof transit.source() !== 'object') { + transit.source(this.org.get(transit.source())); + } + } + + // for checkin, the mbts lives on the main circ + if (circ && circ.billable_transaction()) { + result.mbts = circ.billable_transaction().summary(); + } + + // on renewals, the mbts lives on the parent circ + if (parent_circ && parent_circ.billable_transaction()) { + result.mbts = parent_circ.billable_transaction().summary(); + } + return promise.then(_ => result); } @@ -598,7 +630,6 @@ export class CircService { return this.checkin(params); } - // Alerts that require a manual override. if (allEvents.filter( e => CAN_OVERRIDE_CHECKIN_ALERTS.includes(e.textcode)).length > 0) { @@ -616,12 +647,14 @@ export class CircService { case 'ITEM_NOT_CATALOGED': this.audio.play('error.checkout.no_cataloged'); + result.routeTo = 'Cataloging'; // TODO if (!this.suppressCheckinPopups && !this.ignoreCheckinPrecats) { // Tell the user its a precat and return the result. return this.components.routeToCatalogingDialog.open() .toPromise().then(_ => result); } + break; case 'ROUTE_ITEM': this.components.routeDialog.checkin = result; @@ -651,6 +684,7 @@ export class CircService { if (hold) { if (hold.pickup_lib() === this.auth.user().ws_ou()) { + result.routeTo = 'Holds Shelf'; // TODO this.components.routeDialog.checkin = result; return this.components.routeDialog.open().toPromise() .then(_ => result); @@ -661,8 +695,13 @@ export class CircService { } } else { - console.warn("API Returned insufficient info on holds"); + console.warn('API Returned insufficient info on holds'); } + + case 11: /* CATALOGING */ + this.audio.play('info.checkin.cataloging'); + result.routeTo = 'Cataloging'; // TODO + // TODO more... } return Promise.resolve(result); diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/route-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/route-dialog.component.ts index 94da5273fa..7972bed974 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/route-dialog.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/route-dialog.component.ts @@ -41,7 +41,6 @@ export class RouteDialogComponent extends DialogComponent { } open(ops?: NgbModalOptions): Observable { - // Depending on various settings, the dialog may never open. // But in some cases we still have to collect the data // for printing. @@ -66,7 +65,6 @@ export class RouteDialogComponent extends DialogComponent { } collectData(): Promise { - let promise = Promise.resolve(null); const hold = this.checkin.hold; -- 2.11.0