From d3bb218b6a91e108cf2c05ea17ca14c43501b18c Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Fri, 9 Apr 2021 12:33:53 -0400 Subject: [PATCH] LP1904036 checkin; route dialogs Signed-off-by: Bill Erickson Signed-off-by: Jane Sandberg Signed-off-by: Galen Charlton --- .../src/app/staff/circ/patron/resolver.service.ts | 6 +- .../eg2/src/app/staff/share/circ/circ.module.ts | 2 + .../eg2/src/app/staff/share/circ/circ.service.ts | 106 ++++++++++++++- .../app/staff/share/circ/components.component.html | 8 +- .../app/staff/share/circ/components.component.ts | 2 + .../staff/share/circ/route-dialog.component.html | 15 +++ .../app/staff/share/circ/route-dialog.component.ts | 147 +++++++++++++++++++++ 7 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/share/circ/route-dialog.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/share/circ/route-dialog.component.ts diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/resolver.service.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/resolver.service.ts index db2787c252..2bb85639aa 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/resolver.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/resolver.service.ts @@ -6,14 +6,15 @@ import {NetService} from '@eg/core/net.service'; import {OrgService} from '@eg/core/org.service'; import {AuthService} from '@eg/core/auth.service'; import {PatronContextService} from './patron.service'; - +import {CircService} from '@eg/staff/share/circ/circ.service'; @Injectable() export class PatronResolver implements Resolve> { constructor( private store: ServerStoreService, - private context: PatronContextService + private context: PatronContextService, + private circ: CircService ) {} resolve( @@ -130,6 +131,7 @@ export class PatronResolver implements Resolve> { 'ui.admin.patron_log.max_entries' ]).then(settings => { this.context.settingsCache = settings; + return this.circ.applySettings(); }); } } diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts index dc639e94b2..189d27a138 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts @@ -10,6 +10,7 @@ import {ClaimsReturnedDialogComponent} from './claims-returned-dialog.component' import {CircComponentsComponent} from './components.component'; import {CircEventsComponent} from './events-dialog.component'; import {OpenCircDialogComponent} from './open-circ-dialog.component'; +import {RouteDialogComponent} from './route-dialog.component'; @NgModule({ declarations: [ @@ -19,6 +20,7 @@ import {OpenCircDialogComponent} from './open-circ-dialog.component'; PrecatCheckoutDialogComponent, ClaimsReturnedDialogComponent, CircEventsComponent, + RouteDialogComponent, OpenCircDialogComponent ], imports: [ 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 eb9e5ba1af..e347c1aefe 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 @@ -12,6 +12,7 @@ import {AudioService} from '@eg/share/util/audio.service'; import {CircEventsComponent} from './events-dialog.component'; import {CircComponentsComponent} from './components.component'; import {StringService} from '@eg/share/string/string.service'; +import {ServerStoreService} from '@eg/core/server-store.service'; export interface CircDisplayInfo { title?: string; @@ -147,6 +148,7 @@ export interface CheckinParams { copy_barcode?: string; claims_never_checked_out?: boolean; void_overdues?: boolean; + auto_print_hold_transits?: boolean; // internal tracking _override?: boolean; @@ -159,8 +161,13 @@ export interface CheckinResult { params: CheckinParams; success: boolean; copy?: IdlObject; + volume?: IdlObject; circ?: IdlObject; record?: IdlObject; + hold?: IdlObject; + transit?: IdlObject; + org?: number; + patron?: IdlObject; } @Injectable() @@ -173,6 +180,8 @@ export class CircService { suppressCheckinPopups = false; ignoreCheckinPrecats = false; copyLocationCache: {[id: number]: IdlObject} = {}; + clearHoldsOnCheckout = false; + orgAddrCache: {[addrId: number]: IdlObject} = {}; constructor( private audio: AudioService, @@ -180,11 +189,20 @@ export class CircService { private org: OrgService, private net: NetService, private pcrud: PcrudService, + private serverStore: ServerStoreService, private strings: StringService, private auth: AuthService, private bib: BibRecordService, ) {} + applySettings(): Promise { + return this.serverStore.getItemBatch([ + 'circ.clear_hold_on_checkout', + ]).then(sets => { + this.clearHoldsOnCheckout = sets['circ.clear_hold_on_checkout']; + }); + } + // 'circ' is fleshed with copy, vol, bib, wide_display_entry // Extracts some display info from a fleshed circ. getDisplayInfo(circ: IdlObject): CircDisplayInfo { @@ -218,6 +236,52 @@ export class CircService { }; } + getOrgAddr(orgId: number, addrType): Promise { + const org = this.org.get(orgId); + const addrId = this.org[addrType](); + + if (!addrId) { return Promise.resolve(null); } + + if (this.orgAddrCache[addrId]) { + return Promise.resolve(this.orgAddrCache[addrId]); + } + + return this.pcrud.retrieve('aoa', addrId).toPromise() + .then(addr => { + this.orgAddrCache[addrId] = addr; + return addr; + }); + } + + // find the open transit for the given copy barcode; flesh the org + // units locally. + findCopyTransit(result: CheckinResult): Promise { + // NOTE: evt.payload.transit may exist, but it's not necessarily + // the transit we want, since a transit close + open in the API + // returns the closed transit. + + return this.pcrud.search('atc', + { dest_recv_time : null, cancel_time : null}, + { flesh : 1, + flesh_fields : {atc : ['target_copy']}, + join : { + acp : { + filter : { + barcode : result.params.copy_barcode, + deleted : 'f' + } + } + }, + limit : 1, + order_by : {atc : 'source_send_time desc'}, + }, {authoritative : true} + ).toPromise().then(transit => { + transit.source(this.org.get(transit.source())); + transit.dest(this.org.get(transit.dest())); + return transit; + }); + } + getNonCatTypes(): Promise { if (this.nonCatTypes) { @@ -483,11 +547,14 @@ export class CircService { success: success, circ: payload.circ, copy: payload.copy, - record: payload.record + volume: payload.volume, + record: payload.record, + transit: payload.transit }; let promise = Promise.resolve();; const copy = result.copy; + const volume = result.volume; if (copy) { if (this.copyLocationCache[copy.location()]) { @@ -501,6 +568,22 @@ export class CircService { } } + if (volume) { + // Flesh volume prefixes and suffixes + + if (typeof volume.prefix() !== 'object') { + promise = promise.then(_ => + this.pcrud.retrieve('acnp', volume.prefix()).toPromise() + ).then(p => volume.prefix(p)); + } + + if (typeof volume.suffix() !== 'object') { + promise = promise.then(_ => + this.pcrud.retrieve('acns', volume.suffix()).toPromise() + ).then(p => volume.suffix(p)); + } + } + return promise.then(_ => result); } @@ -558,6 +641,27 @@ export class CircService { case 7: /* RESHELVING */ this.audio.play('success.checkin'); return this.handleCheckinLocAlert(result); + + case 8: /* ON HOLDS SHELF */ + this.audio.play('info.checkin.holds_shelf'); + + const hold = result.hold; + + if (hold) { + + if (hold.pickup_lib() === this.auth.user().ws_ou()) { + this.components.routeDialog.checkin = result; + return this.components.routeDialog.open().toPromise() + .then(_ => result); + + } else { + // Should not happen in practice, but to be safe. + this.audio.play('warning.checkin.wrong_shelf'); + } + + } else { + console.warn("API Returned insufficient info on holds"); + } } return Promise.resolve(result); 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 c287856ec4..5c69ceb636 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 @@ -8,11 +8,13 @@ i18n-dialogBody dialogBody="This item needs to be routed to CATALOGING"> - + + Item {{barcode}} needs to be routed to {{location}}. + + + - diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.ts index d1bcadba0e..2550ea7a5e 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.ts @@ -5,6 +5,7 @@ import {CircEventsComponent} from './events-dialog.component'; import {StringComponent} from '@eg/share/string/string.component'; import {AlertDialogComponent} from '@eg/share/dialog/alert.component'; import {OpenCircDialogComponent} from './open-circ-dialog.component'; +import {RouteDialogComponent} from './route-dialog.component'; /* Container component for sub-components used by circulation actions. * @@ -24,6 +25,7 @@ export class CircComponentsComponent { @ViewChild('routeToCatalogingDialog') routeToCatalogingDialog: AlertDialogComponent; @ViewChild('openCircDialog') openCircDialog: OpenCircDialogComponent; @ViewChild('locationAlertDialog') locationAlertDialog: AlertDialogComponent; + @ViewChild('routeDialog') routeDialog: RouteDialogComponent; constructor(private circ: CircService) { this.circ.components = this; diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/route-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/circ/route-dialog.component.html new file mode 100644 index 0000000000..18505983d8 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/route-dialog.component.html @@ -0,0 +1,15 @@ + + + + + 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 new file mode 100644 index 0000000000..d488a51300 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/route-dialog.component.ts @@ -0,0 +1,147 @@ +import {Component, OnInit, Output, Input, ViewChild, EventEmitter} from '@angular/core'; +import {empty, of, from, Observable} from 'rxjs'; +import {concatMap} from 'rxjs/operators'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {OrgService} from '@eg/core/org.service'; +import {CircService} from './circ.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {AlertDialogComponent} from '@eg/share/dialog/alert.component'; +import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap'; +import {DialogComponent} from '@eg/share/dialog/dialog.component'; +import {CheckinResult} from './circ.service'; +import {ServerStoreService} from '@eg/core/server-store.service'; +import {AudioService} from '@eg/share/util/audio.service'; +import {PrintService} from '@eg/share/print/print.service'; + +/** Route Item Dialog */ + +@Component({ + templateUrl: 'components.component.html', + selector: 'eg-circ-components' +}) +export class RouteDialogComponent extends DialogComponent { + + checkin: CheckinResult; + noAutoPrint: {[template: string]: boolean} = {}; + slip: string; + orgAddress: IdlObject; + destCourierCode: string; + destOrg: IdlObject; + + constructor( + private modal: NgbModal, + private pcrud: PcrudService, + private org: OrgService, + private circ: CircService, + private audio: AudioService, + private print: PrintService, + private serverStore: ServerStoreService) { + super(modal); + } + + open(ops?: NgbModalOptions): Observable { + + return from(this.applySettings()) + + .pipe(concatMap(exit => { + if (exit) { + return of(exit); + } else { + return from(this.collectData()); + } + })) + + .pipe(concatMap(exit => { + if (exit) { + return of(exit); + } else { + return super.open(ops); + } + })); + } + + collectData(): Promise { + + let promise = Promise.resolve(null); + const hold = this.checkin.hold; + + if (this.checkin.org && this.slip !== 'hold_shelf_slip') { + + promise = promise.then(_ => { + return this.circ.getOrgAddr(this.checkin.org, 'holds_address') + .then(addr => this.orgAddress = addr); + }); + } + + if (hold) { + + promise = promise.then(_ => { + return this.pcrud.retrieve('au', hold.usr(), + {flesh: 1, flesh_fields : {'au' : ['card']}}).toPromise() + .then(patron => this.checkin.patron = patron); + }); + } + + if (this.slip !== 'hold_shelf_slip') { + + promise = promise.then(_ => this.circ.findCopyTransit(this.checkin)) + .then(transit => { + this.checkin.transit = transit; + return this.org.settings('lib.courier_code', transit.dest.id()) + .then(sets => this.destCourierCode = sets['lib.courier_code']); + }); + } + + if (this.checkin.transit) { + this.destOrg = this.org.get(this.checkin.transit.dest()); + } + + this.audio.play(hold ? + 'info.checkin.transit.hold' : 'info.checkin.transit'); + + if (this.checkin.params.auto_print_hold_transits + || this.circ.suppressCheckinPopups) { + // Print and exit. + return this.printTransit().then(_ => false); + } + + return promise; + } + + applySettings(): Promise { + + if (this.checkin.transit) { + if (this.checkin.patron) { + this.slip = 'hold_transit_slip'; + } else { + this.slip = 'transit_slip'; + } + } else { + this.slip = 'hold_shelf_slip'; + } + + const autoPrintSet = 'circ.staff_client.do_not_auto_attempt_print'; + + return this.serverStore.getItemBatch([autoPrintSet]).then(sets => { + const autoPrintArr = sets[autoPrintSet]; + + if (Array.isArray(autoPrintArr)) { + this.noAutoPrint['hold_shelf_slip'] = + autoPrintArr.includes('Hold Slip'); + + this.noAutoPrint['hold_transit_slip'] = + autoPrintArr.includes('Hold/Transit Slip'); + + this.noAutoPrint['transit_slip'] = + autoPrintArr.includes('Transit Slip'); + } + }) + .then(_ => this.noAutoPrint[this.slip]); + } + + printTransit(): Promise { + return null; + } +} + -- 2.11.0