From b6303939044039bcc81b748abf86872fee17d334 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Mon, 10 May 2021 18:02:57 -0400 Subject: [PATCH] LPLP1904036 Adding renew interface and entry points Signed-off-by: Bill Erickson Signed-off-by: Jane Sandberg Signed-off-by: Galen Charlton --- .../src/app/staff/circ/renew/renew.component.css | 4 + .../src/app/staff/circ/renew/renew.component.html | 174 ++++++++++++ .../src/app/staff/circ/renew/renew.component.ts | 291 +++++++++++++++++++++ .../eg2/src/app/staff/circ/renew/renew.module.ts | 37 +++ .../eg2/src/app/staff/circ/renew/routing.module.ts | 15 ++ .../src/eg2/src/app/staff/circ/routing.module.ts | 4 + Open-ILS/src/eg2/src/app/staff/nav.component.html | 4 + .../eg2/src/app/staff/share/circ/circ.service.ts | 11 +- .../app/staff/share/circ/components.component.html | 3 + .../src/app/staff/share/worklog/worklog.service.ts | 9 + Open-ILS/src/templates/staff/navbar.tt2 | 7 + 11 files changed, 557 insertions(+), 2 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.css create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/renew/renew.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/renew/routing.module.ts diff --git a/Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.css b/Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.css new file mode 100644 index 0000000000..64d3930cbb --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.css @@ -0,0 +1,4 @@ + +.badge { + font-size: 110%; +} diff --git a/Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.html b/Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.html new file mode 100644 index 0000000000..2808938df3 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.html @@ -0,0 +1,174 @@ + + + + + + + + + + + +
+
+
+
+
+ Barcode +
+ +
+ +
+
+
+
+
+
+
+ + +
+ +
+
+
+
+ + + + + {{r.title}} + + {{r.title}} + + + + + {{r.copy.barcode()}} + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + diff --git a/Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.ts new file mode 100644 index 0000000000..ea40697a9e --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.ts @@ -0,0 +1,291 @@ +import {Component, ViewChild, OnInit, AfterViewInit, HostListener} from '@angular/core'; +import {Location} from '@angular/common'; +import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {empty, from} from 'rxjs'; +import {concatMap} from 'rxjs/operators'; +import {IdlObject, IdlService} from '@eg/core/idl.service'; +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 {Pager} from '@eg/share/util/pager'; +import {CircService, CircDisplayInfo, CheckoutParams, CheckoutResult + } from '@eg/staff/share/circ/circ.service'; +import {BarcodeSelectComponent + } from '@eg/staff/share/barcodes/barcode-select.component'; +import {PrintService} from '@eg/share/print/print.service'; +import {MarkDamagedDialogComponent + } from '@eg/staff/share/holdings/mark-damaged-dialog.component'; +import {CopyAlertsDialogComponent + } from '@eg/staff/share/holdings/copy-alerts-dialog.component'; +import {BucketDialogComponent + } from '@eg/staff/share/buckets/bucket-dialog.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {CancelTransitDialogComponent + } from '@eg/staff/share/circ/cancel-transit-dialog.component'; +import {HoldingsService} from '@eg/staff/share/holdings/holdings.service'; +import {AnonCacheService} from '@eg/share/util/anon-cache.service'; + + +interface RenewGridEntry extends CheckoutResult { + // May need to extend... + foo?: number; // Empty interfaces are not allowed. +} + +const TRIM_LIST_TO = 20; + +@Component({ + templateUrl: 'renew.component.html', + styleUrls: ['renew.component.css'] +}) +export class RenewComponent implements OnInit, AfterViewInit { + renewals: RenewGridEntry[] = []; + autoIndex = 0; + + barcode: string; + dueDate: string; + useDueDate = false; + fineTally = 0; + strictBarcode = false; + trimList = false; + itemNeverCirced: string; + + gridDataSource: GridDataSource = new GridDataSource(); + cellTextGenerator: GridCellTextGenerator; + + private copiesInFlight: {[barcode: string]: boolean} = {}; + + @ViewChild('grid') private grid: GridComponent; + @ViewChild('barcodeSelect') private barcodeSelect: BarcodeSelectComponent; + @ViewChild('markDamagedDialog') private markDamagedDialog: MarkDamagedDialogComponent; + @ViewChild('copyAlertsDialog') private copyAlertsDialog: CopyAlertsDialogComponent; + @ViewChild('itemNeverCircedStr') private itemNeverCircedStr: StringComponent; + @ViewChild('cancelTransitDialog') private cancelTransitDialog: CancelTransitDialogComponent; + + constructor( + private router: Router, + private route: ActivatedRoute, + private ngLocation: Location, + private net: NetService, + private org: OrgService, + private auth: AuthService, + private store: ServerStoreService, + private circ: CircService, + private toast: ToastService, + private printer: PrintService, + private holdings: HoldingsService, + private anonCache: AnonCacheService, + public patronService: PatronService + ) {} + + ngOnInit() { + + this.gridDataSource.getRows = (pager: Pager, sort: any[]) => { + return from(this.renewals); + }; + + this.store.getItemBatch(['circ.renew.strict_barcode']) + .then(sets => { + this.strictBarcode = sets['circ.renew.strict_barcode']; + }).then(_ => this.circ.applySettings()); + } + + ngAfterViewInit() { + this.focusInput(); + } + + focusInput() { + const input = document.getElementById('barcode-input'); + if (input) { input.focus(); } + } + + renew(params?: CheckoutParams, override?: boolean): Promise { + if (!this.barcode) { return Promise.resolve(null); } + + const promise = params ? Promise.resolve(params) : this.collectParams(); + + return promise.then((collectedParams: CheckoutParams) => { + if (!collectedParams) { return null; } + + if (this.copiesInFlight[this.barcode]) { + console.debug('Item ' + this.barcode + ' is already mid-renewal'); + return null; + } + + this.copiesInFlight[this.barcode] = true; + return this.circ.renew(collectedParams); + }) + + .then((result: CheckoutResult) => { + if (result && result.success) { + this.gridifyResult(result); + } + delete this.copiesInFlight[this.barcode]; + this.resetForm(); + return result; + }) + + .finally(() => delete this.copiesInFlight[this.barcode]); + } + + collectParams(): Promise { + + const params: CheckoutParams = { + copy_barcode: this.barcode, + due_date: this.useDueDate ? this.dueDate : null, + _renewal: true + }; + + 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: CheckoutResult) { + const entry: RenewGridEntry = result; + entry.index = this.autoIndex++; + + 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.renewals.unshift(entry); + + if (this.trimList && this.renewals.length >= TRIM_LIST_TO) { + this.renewals.length = TRIM_LIST_TO; + } + this.grid.reload(); + } + + toggleStrictBarcode(active: boolean) { + if (active) { + this.store.setItem('circ.checkin.strict_barcode', true); + } else { + this.store.removeItem('circ.checkin.strict_barcode'); + } + } + + printReceipt() { + if (this.renewals.length === 0) { return; } + + this.printer.print({ + printContext: 'default', + templateName: 'renew', + contextData: {renewals: this.renewals} + }); + } + + getCopyIds(rows: RenewGridEntry[], skipStatus?: number): number[] { + return this.getCopies(rows, skipStatus).map(c => Number(c.id())); + } + + getCopies(rows: RenewGridEntry[], skipStatus?: number): IdlObject[] { + let copies = rows.filter(r => r.copy).map(r => r.copy); + if (skipStatus) { + copies = copies.filter( + c => Number(c.status().id()) !== Number(skipStatus)); + } + return copies; + } + + + markDamaged(rows: RenewGridEntry[]) { + const copyIds = this.getCopyIds(rows, 14 /* ignore damaged */); + if (copyIds.length === 0) { return; } + + from(copyIds).pipe(concatMap(id => { + this.markDamagedDialog.copyId = id; + return this.markDamagedDialog.open({size: 'lg'}); + })); + } + + addItemAlerts(rows: RenewGridEntry[]) { + const copyIds = this.getCopyIds(rows); + if (copyIds.length === 0) { return; } + + this.copyAlertsDialog.copyIds = copyIds; + this.copyAlertsDialog.mode = 'create'; + this.copyAlertsDialog.open({size: 'lg'}).subscribe(); + } + + manageItemAlerts(rows: RenewGridEntry[]) { + const copyIds = this.getCopyIds(rows); + if (copyIds.length === 0) { return; } + + this.copyAlertsDialog.copyIds = copyIds; + this.copyAlertsDialog.mode = 'manage'; + this.copyAlertsDialog.open({size: 'lg'}).subscribe(); + } + + retrieveLastPatron(rows: RenewGridEntry[]) { + const copy = this.getCopies(rows).pop(); + if (!copy) { return; } + + this.circ.lastCopyCirc(copy.id()).then(circ => { + if (circ) { + this.router.navigate(['/staff/circ/patron', circ.usr(), 'checkout']); + } else { + this.itemNeverCirced = copy.barcode(); + setTimeout(() => this.toast.danger(this.itemNeverCircedStr.text)); + } + }); + } + + cancelTransits(rows: RenewGridEntry[]) { + + rows = rows.filter(row => row.copy && row.copy.status().id() === 6); + + // Copies in transit are not always accompanied by their transit. + from(rows).pipe(concatMap(row => { + return from( + this.circ.findCopyTransit(row) + .then(transit => row.transit = transit) + ); + })) + .pipe(concatMap(_ => { + + const ids = rows + .filter(row => Boolean(row.transit)) + .map(row => row.transit.id()); + + if (ids.length > 0) { + this.cancelTransitDialog.transitIds = ids; + return this.cancelTransitDialog.open(); + } else { + return empty(); + } + + })).subscribe(); + } + + showRecentCircs(rows: RenewGridEntry[]) { + const copyId = this.getCopyIds(rows)[0]; + if (copyId) { + const url = `/eg/staff/cat/item/${copyId}/circs`; + window.open(url); + } + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/circ/renew/renew.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/renew/renew.module.ts new file mode 100644 index 0000000000..a2c304dc66 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/renew/renew.module.ts @@ -0,0 +1,37 @@ +import {NgModule} from '@angular/core'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {RenewRoutingModule} 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 {RenewComponent} from './renew.component'; +import {WorkLogModule} from '@eg/staff/share/worklog/worklog.module'; + +@NgModule({ + declarations: [ + RenewComponent + ], + imports: [ + StaffCommonModule, + RenewRoutingModule, + FmRecordEditorModule, + BillingModule, + CircModule, + HoldsModule, + HoldingsModule, + BookingModule, + PatronModule, + BarcodesModule, + WorkLogModule + ], + providers: [ + ] +}) + +export class RenewModule {} + diff --git a/Open-ILS/src/eg2/src/app/staff/circ/renew/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/renew/routing.module.ts new file mode 100644 index 0000000000..3f6718c1ea --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/renew/routing.module.ts @@ -0,0 +1,15 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {RenewComponent} from './renew.component'; + +const routes: Routes = [{ + path: '', + component: RenewComponent +}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) + +export class RenewRoutingModule {} 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 5122ec2ffa..9a151cf8f6 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 @@ -17,6 +17,10 @@ const routes: Routes = [{ path: 'checkin', loadChildren: () => import('./checkin/checkin.module').then(m => m.CheckinModule) +}, { + path: 'renew', + loadChildren: () => + import('./renew/renew.module').then(m => m.RenewModule) }]; @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 b7afa93f66..cf29ea972c 100644 --- a/Open-ILS/src/eg2/src/app/staff/nav.component.html +++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html @@ -85,6 +85,10 @@ Renew Items + + + Renew Items (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 cf8736c282..2f4cf01d3f 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 @@ -148,6 +148,7 @@ export interface CircResultCommon { patron?: IdlObject; transit?: IdlObject; copyAlerts?: IdlObject[]; + mbts?: IdlObject; // Calculated values title?: string; @@ -187,7 +188,6 @@ export interface CheckinParams { export interface CheckinResult extends CircResultCommon { params: CheckinParams; - mbts?: IdlObject; routeTo?: string; // org name or in-branch destination destOrg?: IdlObject; destAddress?: IdlObject; @@ -437,7 +437,7 @@ export class CircService { result.nonCatCirc = payload.noncat_circ; return this.fleshCommonData(result).then(_ => { - const action = params._renewal ? 'renewal' : + const action = params._renewal ? 'renew' : (params.noncat ? 'noncat_checkout' : 'checkout'); this.addWorkLog(action, result); return result; @@ -482,6 +482,13 @@ export class CircService { barcode: result.params.copy_barcode }); + case 'ASSET_COPY_NOT_FOUND': + this.audio.play(`error.${key}.not_found`); + return this.exitAlert({ + textcode: result.firstEvent.textcode, + barcode: result.params.copy_barcode + }); + default: this.audio.play(`error.${key}.unknown`); return this.exitAlert({ diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.html b/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.html index 59f437373b..fa41b3ca6b 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.html +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.html @@ -50,6 +50,9 @@ + + Item "{{barcode}}" is marked as Claims Returned to your component for worklog support'); return; } +console.log('1'); entry.when = new Date(); entry.actor = this.auth.user().usrname(); + console.log(`worklog_${entry.action}`); + console.log(this.workLogStrings[`worklog_${entry.action}`]); entry.msg = this.workLogStrings[`worklog_${entry.action}`].text; +console.log('1'); const workLog: WorkLogEntry[] = this.store.getLocalItem('eg.work_log') || []; let patronLog: WorkLogEntry[] = this.store.getLocalItem('eg.patron_log') || []; +console.log('1'); workLog.push(entry); if (workLog.length > this.maxEntries) { workLog.shift(); } +console.log('1'); this.store.setLocalItem('eg.work_log', workLog); +console.log('1'); if (entry.patron_id) { // Remove existing entries that match this patron patronLog = patronLog.filter(e => e.patron_id !== entry.patron_id); +console.log('1'); patronLog.push(entry); if (patronLog.length > this.maxPatrons) { diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2 index bc1d4e1e93..faba109ad3 100644 --- a/Open-ILS/src/templates/staff/navbar.tt2 +++ b/Open-ILS/src/templates/staff/navbar.tt2 @@ -141,6 +141,13 @@ [% l('Renew Items') %] +
  • + + + [% l('Renew Items (Experimental)') %] + +
  • +