From 6c3f9d2ba34d521a1bb4d443f90d480dd973439d Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Thu, 15 Apr 2021 12:44:01 -0400 Subject: [PATCH] LP1904036 copy alerts management (checkin) Signed-off-by: Bill Erickson Signed-off-by: Jane Sandberg Signed-off-by: Galen Charlton --- .../app/staff/circ/checkin/checkin.component.html | 3 + .../app/staff/circ/patron/checkout.component.html | 10 ++ .../app/staff/circ/patron/checkout.component.ts | 17 ++- .../eg2/src/app/staff/share/circ/circ.service.ts | 82 +++++++++--- .../app/staff/share/circ/components.component.html | 2 + .../app/staff/share/circ/components.component.ts | 3 + .../holdings/copy-alert-manager.component.html | 80 ++++++++++++ .../share/holdings/copy-alert-manager.component.ts | 138 +++++++++++++++++++++ .../app/staff/share/holdings/holdings.module.ts | 7 +- .../app/staff/share/holdings/holdings.service.ts | 14 +++ 10 files changed, 335 insertions(+), 21 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alert-manager.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alert-manager.component.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 index 91a64b3c9b..c2f2aeb652 100644 --- 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 @@ -100,6 +100,9 @@ + + diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html index 6ef7700410..3e99c338c6 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html @@ -77,6 +77,13 @@ {{r.title}} + + + +
+ +
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts index 7c7512aea2..e0603442cf 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts @@ -5,6 +5,7 @@ import {tap, switchMap} from 'rxjs/operators'; import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap'; import {IdlObject} from '@eg/core/idl.service'; import {OrgService} from '@eg/core/org.service'; +import {PcrudService} from '@eg/core/pcrud.service'; import {NetService} from '@eg/core/net.service'; import {PatronService} from '@eg/staff/share/patron/patron.service'; import {PatronContextService, CircGridEntry} from './patron.service'; @@ -53,6 +54,7 @@ export class CheckoutComponent implements OnInit, AfterViewInit { private store: StoreService, private serverStore: ServerStoreService, private org: OrgService, + private pcrud: PcrudService, private net: NetService, public circ: CircService, public patronService: PatronService, @@ -175,7 +177,7 @@ export class CheckoutComponent implements OnInit, AfterViewInit { copy: result.copy, circ: result.circ, dueDate: null, - copyAlertCount: 0, // TODO + copyAlertCount: 0, nonCatCount: 0, title: result.title, author: result.author, @@ -188,11 +190,16 @@ export class CheckoutComponent implements OnInit, AfterViewInit { entry.dueDate = result.nonCatCirc.duedate(); entry.nonCatCount = result.params.noncat_count; - } else { + } else if (result.circ) { + entry.dueDate = result.circ.due_date(); + } - if (result.circ) { - entry.dueDate = result.circ.due_date(); - } + if (entry.copy) { + // Fire and forget this one + + this.pcrud.search('aca', + {copy : entry.copy.id(), ack_time : null}, {}, {atomic: true} + ).subscribe(alerts => entry.copyAlertCount = alerts.length); } this.context.checkouts.unshift(entry); 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 eb51abc44b..111122c0ea 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 @@ -13,6 +13,7 @@ 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'; +import {HoldingsService} from '@eg/staff/share/holdings/holdings.service'; export interface CircDisplayInfo { title?: string; @@ -123,6 +124,7 @@ export interface CheckoutParams { dummy_isbn?: string; circ_modifier?: string; void_overdues?: boolean; + new_copy_alerts?: boolean; // internal tracking _override?: boolean; @@ -143,6 +145,7 @@ export interface CircResultCommon { hold?: IdlObject; patron?: IdlObject; transit?: IdlObject; + copyAlerts?: IdlObject[]; // Calculated values title?: string; @@ -165,6 +168,9 @@ export interface CheckinParams { void_overdues?: boolean; auto_print_hold_transits?: boolean; backdate?: string; + capture?: string; + next_copy_status?: number[]; + new_copy_alerts?: boolean; // internal / local values that are moved from the API request. _override?: boolean; @@ -201,7 +207,8 @@ export class CircService { private serverStore: ServerStoreService, private strings: StringService, private auth: AuthService, - private bib: BibRecordService, + private holdings: HoldingsService, + private bib: BibRecordService ) {} applySettings(): Promise { @@ -358,6 +365,7 @@ export class CircService { checkout(params: CheckoutParams): Promise { + params.new_copy_alerts = true; params._renewal = false; console.debug('checking out with', params); @@ -373,6 +381,7 @@ export class CircService { renew(params: CheckoutParams): Promise { + params.new_copy_alerts = true; params._renewal = true; console.debug('renewing out with', params); @@ -621,6 +630,7 @@ export class CircService { } checkin(params: CheckinParams): Promise { + params.new_copy_alerts = true; console.debug('checking in with', params); @@ -648,7 +658,7 @@ export class CircService { result.author = result.record.author(); result.isbn = result.record.isbn(); - } else if (result.copy) { + } else if (copy) { result.title = result.copy.dummy_title(); result.author = result.copy.dummy_author(); result.isbn = result.copy.dummy_isbn(); @@ -664,6 +674,15 @@ export class CircService { this.copyLocationCache[loc.id()] = loc; }); } + + if (typeof copy.status() !== 'object') { + promise = promise.then(_ => this.holdings.getCopyStatuses()) + .then(stats => { + const stat = + Object.values(stats).filter(s => s.id() === copy.status())[0]; + if (stat) { copy.status(stat); } + }); + } } if (volume) { @@ -689,7 +708,8 @@ export class CircService { const allEvents = Array.isArray(response) ? response.map(r => this.evt.parse(r)) : [this.evt.parse(response)]; - console.debug('checkin returned', allEvents.map(e => e.textcode)); + console.debug('checkin events', allEvents.map(e => e.textcode)); + console.debug('checkin response', response); const firstEvent = allEvents[0]; const payload = firstEvent.payload; @@ -764,11 +784,7 @@ export class CircService { // Alerts that require a manual override. if (allEvents.filter( e => CAN_OVERRIDE_CHECKIN_ALERTS.includes(e.textcode)).length > 0) { - - // Should not be necessary, but good to be safe. - if (params._override) { return Promise.resolve(null); } - - return this.showOverrideDialog(result, allEvents, true); + return this.handleOverridableCheckinEvents(result); } switch (result.firstEvent.textcode) { @@ -809,8 +825,14 @@ export class CircService { } handleCheckinSuccess(result: CheckinResult): Promise { + const copy = result.copy; + + if (!copy) { return Promise.resolve(result); } + + const stat = copy.status(); + const statId = typeof stat === 'object' ? stat.id() : stat; - switch (result.copy.status()) { + switch (statId) { case 0: /* AVAILABLE */ case 4: /* MISSING */ @@ -852,9 +874,8 @@ export class CircService { default: this.audio.play('success.checkin'); - const stat = result.copy; console.debug(`Unusual checkin copy status (may have been - set via copy alert): ${stat.id()} : ${stat.name()}`); + set via copy alert): status=${statId}`); } return Promise.resolve(result); @@ -896,15 +917,48 @@ export class CircService { } - handleOverridableCheckinEvents( - result: CheckinResult, events: EgEvent[]): Promise { + handleOverridableCheckinEvents(result: CheckinResult): Promise { const params = result.params; - const firstEvent = events[0]; + const events = result.allEvents; + const firstEvent = result.firstEvent if (params._override) { // Should never get here. Just being safe. return Promise.reject(null); } + + if (this.suppressCheckinPopups && events.filter( + e => !CAN_SUPPRESS_CHECKIN_ALERTS.includes(e.textcode)).length === 0) { + // These events are automatically overridden when suppress + // popups are in effect. + params._override = true; + return this.checkin(params); + } + + // New-style alerts are reported via COPY_ALERT_MESSAGE and + // includes the alerts in the payload as an array. + if (firstEvent.textcode === 'COPY_ALERT_MESSAGE' + && Array.isArray(firstEvent.payload)) { + this.components.copyAlertManager.alerts = firstEvent.payload; + this.components.copyAlertManager.mode = 'checkin'; + + return this.components.copyAlertManager.open().toPromise() + .then(resp => { + + if (!resp) { return result; } // dialog was canceled + + if (resp.nextStatus !== null) { + params.next_copy_status = [resp.nextStatus]; + params.capture = 'nocapture'; + } + + params._override = true; + + return this.checkin(params); + }); + } + + return this.showOverrideDialog(result, events, true); } 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 3a29fccb53..a937b4bb64 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 @@ -59,3 +59,5 @@ + + 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 c7f82023bd..76dc5f5aab 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 @@ -7,6 +7,8 @@ import {AlertDialogComponent} from '@eg/share/dialog/alert.component'; import {OpenCircDialogComponent} from './open-circ-dialog.component'; import {RouteDialogComponent} from './route-dialog.component'; import {CopyInTransitDialogComponent} from './in-transit-dialog.component'; +import {CopyAlertManagerDialogComponent + } from '@eg/staff/share/holdings/copy-alert-manager.component'; /* Container component for sub-components used by circulation actions. * @@ -30,6 +32,7 @@ export class CircComponentsComponent { @ViewChild('circFailedDialog') circFailedDialog: AlertDialogComponent; @ViewChild('routeDialog') routeDialog: RouteDialogComponent; @ViewChild('copyInTransitDialog') copyInTransitDialog: CopyInTransitDialogComponent; + @ViewChild('copyAlertManager') copyAlertManager: CopyAlertManagerDialogComponent; @ViewChild('holdShelfStr') holdShelfStr: StringComponent; @ViewChild('catalogingStr') catalogingStr: StringComponent; diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alert-manager.component.html b/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alert-manager.component.html new file mode 100644 index 0000000000..8bf3be0302 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alert-manager.component.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alert-manager.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alert-manager.component.ts new file mode 100644 index 0000000000..f6a4740d68 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/copy-alert-manager.component.ts @@ -0,0 +1,138 @@ +import {Component, OnInit, Input, ViewChild} from '@angular/core'; +import {Observable, throwError, from} from 'rxjs'; +import {concatMap} from 'rxjs/operators'; +import {NetService} from '@eg/core/net.service'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {EventService} from '@eg/core/event.service'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {AuthService} from '@eg/core/auth.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {OrgService} from '@eg/core/org.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {StringService} from '@eg/share/string/string.service'; +import {DialogComponent} from '@eg/share/dialog/dialog.component'; +import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap'; +import {ComboboxEntry} from '@eg/share/combobox/combobox.component'; +import {HoldingsService} from './holdings.service'; + +/** + * Dialog for managing copy alerts. + */ + +@Component({ + selector: 'eg-copy-alert-manager', + templateUrl: 'copy-alert-manager.component.html', + styles: ['.acknowledged {text-decoration: line-through }'] +}) + +export class CopyAlertManagerDialogComponent + extends DialogComponent implements OnInit { + + mode: string; + alerts: IdlObject[]; + nextStatuses: IdlObject[]; + nextStatus: number; + + constructor( + private modal: NgbModal, + private toast: ToastService, + private net: NetService, + private idl: IdlService, + private pcrud: PcrudService, + private org: OrgService, + private auth: AuthService, + private strings: StringService, + private holdings: HoldingsService + ) { super(modal); } + + ngOnInit() {} + + open(ops?: NgbModalOptions): Observable { + + this.nextStatus = null; + + let promise = Promise.resolve(null); + this.alerts.forEach(copyAlert => + promise = promise.then(_ => this.ingestAlert(copyAlert))); + + return from(promise).pipe(concatMap(_ => super.open(ops))); + } + + ingestAlert(copyAlert: IdlObject): Promise { + let promise = Promise.resolve(null); + + const state = copyAlert.alert_type().state(); + copyAlert._event = copyAlert.alert_type().event(); + + if (copyAlert.note()) { + copyAlert._message = copyAlert.note(); + } else { + const key = `staff.holdings.copyalert.${copyAlert._event}.${state}`; + promise = promise.then(_ => { + return this.strings.interpolate(key) + .then(str => copyAlert._message = str); + }); + } + + const nextStatuses: number[] = []; + this.nextStatuses = []; + + if (copyAlert.temp() === 'f') { return promise; } + + copyAlert.alert_type().next_status().forEach(statId => { + if (!nextStatuses.includes(statId)) { + nextStatuses.push(statId); + } + }); + + if (this.mode === 'checkin' && nextStatuses.length > 0) { + + promise = promise.then(_ => this.holdings.getCopyStatuses()) + .then(statMap => { + nextStatuses.forEach(statId => { + const wanted = statMap[statId]; + if (wanted) { this.nextStatuses.push(wanted); } + }) + + if (this.nextStatuses.length > 0) { + this.nextStatus = this.nextStatuses[0].id(); + } + }); + } + + return promise; + } + + canBeAcked(copyAlert: IdlObject): boolean { + return !copyAlert.ack_time() && copyAlert.temp() === 't'; + } + + canBeRemoved(copyAlert: IdlObject): boolean { + return !copyAlert.ack_time() && copyAlert.temp() === 'f'; + } + + isAcked(copyAlert: IdlObject): boolean { + return copyAlert._acked; + } + + ok() { + const acks: IdlObject[] = []; + this.alerts.forEach(copyAlert => { + + if (copyAlert._acked) { + copyAlert.ack_time('now'); + copyAlert.ack_staff(this.auth.user().id()); + copyAlert.ischanged(true); + acks.push(copyAlert); + } + + if (acks.length > 0) { + this.pcrud.update(acks).toPromise() + .then(_ => this.close({nextStatus: this.nextStatus})); + } else { + this.close({nextStatus: this.nextStatus}); + } + }); + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.module.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.module.ts index 96357a6712..fd1ec0f680 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.module.ts @@ -14,6 +14,7 @@ import {ConjoinedItemsDialogComponent} from './conjoined-items-dialog.component' import {TransferItemsComponent} from './transfer-items.component'; import {TransferHoldingsComponent} from './transfer-holdings.component'; import {BatchItemAttrComponent} from './batch-item-attr.component'; +import {CopyAlertManagerDialogComponent} from './copy-alert-manager.component'; @NgModule({ declarations: [ @@ -28,7 +29,8 @@ import {BatchItemAttrComponent} from './batch-item-attr.component'; ConjoinedItemsDialogComponent, TransferItemsComponent, TransferHoldingsComponent, - BatchItemAttrComponent + BatchItemAttrComponent, + CopyAlertManagerDialogComponent ], imports: [ StaffCommonModule, @@ -46,7 +48,8 @@ import {BatchItemAttrComponent} from './batch-item-attr.component'; ConjoinedItemsDialogComponent, TransferItemsComponent, TransferHoldingsComponent, - BatchItemAttrComponent + BatchItemAttrComponent, + CopyAlertManagerDialogComponent ], providers: [ HoldingsService diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts index 00cd34c753..6b21646780 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/holdings.service.ts @@ -3,6 +3,7 @@ */ import {Injectable, EventEmitter} from '@angular/core'; import {IdlObject, IdlService} from '@eg/core/idl.service'; +import {tap} from 'rxjs/operators'; import {NetService} from '@eg/core/net.service'; import {AnonCacheService} from '@eg/share/util/anon-cache.service'; import {PcrudService} from '@eg/core/pcrud.service'; @@ -20,6 +21,8 @@ export interface CallNumData { @Injectable() export class HoldingsService { + copyStatuses: {[id: number]: IdlObject}; + constructor( private net: NetService, private auth: AuthService, @@ -93,5 +96,16 @@ export class HoldingsService { 18 // Canceled Transit ]); } + + getCopyStatuses(): Promise<{[id: number]: IdlObject}> { + if (this.copyStatuses) { + return Promise.resolve(this.copyStatuses); + } + + this.copyStatuses = {}; + return this.pcrud.retrieveAll('ccs', {order_by: {ccs: 'name'}}) + .pipe(tap(stat => this.copyStatuses[stat.id()] = stat)) + .toPromise().then(_ => this.copyStatuses); + } } -- 2.11.0