LP1904036 events and overrides
authorBill Erickson <berickxx@gmail.com>
Mon, 1 Mar 2021 19:57:29 +0000 (14:57 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:25 +0000 (20:13 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/circ/patron/resolver.service.ts
Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts
Open-ILS/src/eg2/src/app/staff/share/circ/components.component.html
Open-ILS/src/eg2/src/app/staff/share/circ/components.component.ts
Open-ILS/src/eg2/src/app/staff/share/circ/events-dialog.component.html
Open-ILS/src/eg2/src/app/staff/share/circ/events-dialog.component.ts
Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.ts
Open-ILS/src/eg2/src/styles.css

index 8ddef06..e9ea79d 100644 (file)
@@ -19,17 +19,23 @@ export class PatronResolver implements Resolve<Promise<any[]>> {
     resolve(
         route: ActivatedRouteSnapshot,
         state: RouterStateSnapshot): Promise<any[]> {
-
         return this.fetchSettings();
     }
 
     fetchSettings(): Promise<any> {
 
+        // Some of these are used by the shared circ service.
+        // Go ahead and precache them since we're making the call anyway.
         return this.store.getItemBatch([
           'eg.circ.patron.summary.collapse',
           'circ.do_not_tally_claims_returned',
-          'circ.tally_lost'
-
+          'circ.tally_lost',
+          'ui.staff.require_initials.patron_standing_penalty',
+          'ui.admin.work_log.max_entries',
+          'ui.admin.patron_log.max_entries',
+          'circ.staff_client.do_not_auto_attempt_print',
+          'circ.clear_hold_on_checkout',
+          'ui.circ.suppress_checkin_popups'
         ]).then(settings => {
             this.context.noTallyClaimsReturned =
                 settings['circ.do_not_tally_claims_returned'];
index e7ef216..3b84242 100644 (file)
@@ -111,8 +111,10 @@ export interface CheckoutParams {
     dummy_author?: string;
     dummy_isbn?: string;
     circ_modifier?: string;
-    _override?: boolean; // internal tracking
-    _renewal?: boolean; // internal tracking
+
+    // internal tracking
+    _override?: boolean;
+    _renewal?: boolean;
 }
 
 export interface CheckoutResult {
@@ -133,7 +135,9 @@ export interface CheckinParams {
     copy_id?: number;
     copy_barcode?: string;
     claims_never_checked_out?: boolean;
-    _override?: boolean; // internal tracking
+
+    // internal tracking
+    _override?: boolean;
 }
 
 export interface CheckinResult {
@@ -154,6 +158,8 @@ export class CircService {
     components: CircComponentsComponent;
     nonCatTypes: IdlObject[] = null;
     autoOverrideCheckoutEvents: {[textcode: string]: boolean} = {};
+    suppressCheckinPopups = false;
+    ignoreCheckinPrecats = false;
 
     constructor(
         private audio: AudioService,
@@ -183,7 +189,7 @@ export class CircService {
     apiParams(
         params: CheckoutParams | CheckinParams): CheckoutParams | CheckinParams {
 
-        const apiParams = Object.assign(params, {}); // clone
+        const apiParams = Object.assign({}, params); // clone
         const remove = Object.keys(apiParams).filter(k => k.match(/^_/));
         remove.forEach(p => delete apiParams[p]);
 
@@ -247,8 +253,11 @@ export class CircService {
             nonCatCirc: payload.noncat_circ
         };
 
+        const overridable = result.params._renewal ?
+            CAN_OVERRIDE_RENEW_EVENTS : CAN_OVERRIDE_CHECKOUT_EVENTS;
+
         if (allEvents.filter(
-            e => CAN_OVERRIDE_RENEW_EVENTS.includes(e.textcode)).length > 0) {
+            e => overridable.includes(e.textcode)).length > 0) {
             return this.handleOverridableCheckoutEvents(result, allEvents);
         }
 
@@ -256,6 +265,7 @@ export class CircService {
             case 'SUCCESS':
                 result.success = true;
                 this.audio.play('success.checkout');
+                break;
 
             case 'ITEM_NOT_CATALOGED':
                 return this.handlePrecat(result);
@@ -285,28 +295,31 @@ export class CircService {
         return this.showOverrideDialog(result, events);
     }
 
-    showOverrideDialog(
-        result: CheckoutResult, events: EgEvent[]): Promise<CheckoutResult> {
+    showOverrideDialog(result: CheckoutResult,
+        events: EgEvent[], checkin?: boolean): Promise<CheckoutResult> {
+
         const params = result.params;
+        const mode = checkin ? 'checkin' : (params._renewal ? 'renew' : 'checkout');
 
         this.components.circEventsDialog.events = events;
-        // TODO: support checkins too
-        this.components.circEventsDialog.mode = params._renewal ? 'renew' : 'checkout';
+        this.components.circEventsDialog.mode = mode;
 
         return this.components.circEventsDialog.open().toPromise()
         .then(confirmed => {
             if (!confirmed) { return null; }
 
-            // Indicate these events have been seen and overridden.
-            events.forEach(evt => {
-                if (CHECKOUT_OVERRIDE_AFTER_FIRST.includes(evt.textcode)) {
-                    this.autoOverrideCheckoutEvents[evt.textcode] = true;
-                }
-            });
+            if (!checkin) {
+                // Indicate these events have been seen and overridden.
+                events.forEach(evt => {
+                    if (CHECKOUT_OVERRIDE_AFTER_FIRST.includes(evt.textcode)) {
+                        this.autoOverrideCheckoutEvents[evt.textcode] = true;
+                    }
+                });
+            }
 
             params._override = true;
 
-            return params._renewal ? this.renew(params) : this.checkout(params);
+            return this[mode](params); // checkout/renew/checkin
         });
     }
 
@@ -345,9 +358,10 @@ export class CircService {
 
         console.debug('checkout resturned', response);
 
-        const firstResp = Array.isArray(response) ? response[0] : response;
+        const allEvents = Array.isArray(response) ?
+            response.map(r => this.evt.parse(r)) : [this.evt.parse(response)];
 
-        const firstEvent = this.evt.parse(firstResp);
+        const firstEvent = allEvents[0];
         const payload = firstEvent.payload;
 
         if (!payload) {
@@ -355,12 +369,6 @@ export class CircService {
             return Promise.reject();
         }
 
-        switch (firstEvent.textcode) {
-            case 'ITEM_NOT_CATALOGED':
-                this.audio.play('error.checkout.no_cataloged');
-                // alert, etc.
-        }
-
         const success =
             firstEvent.textcode.match(/SUCCESS|NO_CHANGE|ROUTE_ITEM/) !== null;
 
@@ -375,17 +383,73 @@ export class CircService {
             record: payload.record
         };
 
+        // Informational alerts that can be ignored if configured.
+        if (this.suppressCheckinPopups &&
+            allEvents.filter(e =>
+                !CAN_SUPPRESS_CHECKIN_ALERTS.includes(e.textcode)).length == 0) {
+
+            // Should not be necessary, but good to be safe.
+            if (params._override) { return Promise.resolve(null); }
+
+            params._override = true;
+            return this.checkin(params);
+        }
+
+
+        // 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);
+        }
+
+        switch (firstEvent.textcode) {
+            case 'SUCCESS':
+            case 'NO_CHANGE':
+                this.audio.play('success.checkin');
+                // TODO do copy status stuff
+                break;
+
+            case 'ITEM_NOT_CATALOGED':
+                this.audio.play('error.checkout.no_cataloged');
+
+                if (!this.suppressCheckinPopups && !this.ignoreCheckinPrecats) {
+                    // Tell the user its a precat and return the result.
+                    return this.components.routeToCatalogingDialog.open()
+                    .toPromise().then(_ => result);
+                }
+
+                // alert, etc.
+        }
+
         return Promise.resolve(result);
     }
 
+    handleOverridableCheckinEvents(
+        result: CheckinResult, events: EgEvent[]): Promise<CheckinResult> {
+        const params = result.params;
+        const firstEvent = events[0];
+
+        if (params._override) {
+            // Should never get here.  Just being safe.
+            return Promise.reject(null);
+        }
+
+    }
+
+
     // The provided params (minus the copy_id) will be used
     // for all items.
-    checkoutBatch(copyIds: number[], params: CheckoutParams): Observable<CheckoutResult> {
+    checkoutBatch(copyIds: number[],
+        params: CheckoutParams): Observable<CheckoutResult> {
+
         if (copyIds.length === 0) { return empty(); }
-        const source = from(copyIds);
 
-        return source.pipe(concatMap(id => {
-            const cparams = Object.assign(params, {}); // clone
+        return from(copyIds).pipe(concatMap(id => {
+            const cparams = Object.assign({}, params); // clone
             cparams.copy_id = id;
             return from(this.checkout(cparams));
         }));
@@ -393,15 +457,14 @@ export class CircService {
 
     // The provided params (minus the copy_id) will be used
     // for all items.
-    renewBatch(copyIds: number[], params?: CheckoutParams): Observable<CheckoutResult> {
-        if (copyIds.length === 0) { return empty(); }
+    renewBatch(copyIds: number[],
+        params?: CheckoutParams): Observable<CheckoutResult> {
 
+        if (copyIds.length === 0) { return empty(); }
         if (!params) { params = {}; }
 
-        const source = from(copyIds);
-
-        return source.pipe(concatMap(id => {
-            const cparams = Object.assign(params, {}); // clone
+        return from(copyIds).pipe(concatMap(id => {
+            const cparams = Object.assign({}, params); // clone
             cparams.copy_id = id;
             return from(this.renew(cparams));
         }));
@@ -409,15 +472,14 @@ export class CircService {
 
     // The provided params (minus the copy_id) will be used
     // for all items.
-    checkinBatch(copyIds: number[], params?: CheckinParams): Observable<CheckinResult> {
-        if (copyIds.length === 0) { return empty(); }
+    checkinBatch(copyIds: number[],
+        params?: CheckinParams): Observable<CheckinResult> {
 
+        if (copyIds.length === 0) { return empty(); }
         if (!params) { params = {}; }
 
-        const source = from(copyIds);
-
-        return source.pipe(concatMap(id => {
-            const cparams = Object.assign(params, {}); // clone
+        return from(copyIds).pipe(concatMap(id => {
+            const cparams = Object.assign({}, params); // clone
             cparams.copy_id = id;
             return from(this.checkin(cparams));
         }));
index 7817e9b..d29bc19 100644 (file)
@@ -1,4 +1,8 @@
 
 <eg-precat-checkout-dialog #precatDialog></eg-precat-checkout-dialog>
 <eg-circ-events-dialog #circEventsDialog></eg-circ-events-dialog>
+<eg-alert-dialog #routeToCatalogingDialog
+  i18n-dialogTitle dialogTitle="Route To Cataloging"
+  i18n-dialogBody dialogBody="This item needs to be routed to CATALOGING">
+</eg-alert-dialog>
 
index 1d8cc15..7b506ff 100644 (file)
@@ -2,6 +2,8 @@ import {Component, OnInit, Output, Input, ViewChild, EventEmitter} from '@angula
 import {CircService} from './circ.service';
 import {PrecatCheckoutDialogComponent} from './precat-dialog.component';
 import {CircEventsComponent} from './events-dialog.component';
+import {StringComponent} from '@eg/share/string/string.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
 
 /* Container component for sub-components used by circulation actions.
  *
@@ -18,6 +20,7 @@ export class CircComponentsComponent {
 
     @ViewChild('precatDialog') precatDialog: PrecatCheckoutDialogComponent;
     @ViewChild('circEventsDialog') circEventsDialog: CircEventsComponent;
+    @ViewChild('routeToCatalogingDialog') routeToCatalogingDialog: AlertDialogComponent;
 
     constructor(private circ: CircService) {
         this.circ.components = this;
index e670b6e..0b83e0c 100644 (file)
     <ng-container *ngFor="let evt of events">
       <div class="card mb-2">
         <div class="card-header text-danger">{{evt.textcode}}</div>
-        <div class="card-body">{{evt.desc}}</div>
+        <div class="card-body">
+          <div>{{evt.desc}}</div>
+          <ng-container *ngIf="evt.textcode == 'COPY_ALERT_MESSAGE'">
+            <ng-container *ngIf="!isArray(evt.payload)">
+              <!-- Traditional copy.alert_message value in payload -->
+              <div class="mt-2 font-weight-bold">{{evt.payload}}</div>
+            </ng-container>
+          </ng-container>
+        </div>
       </div>
     </ng-container>
   </div>
index 006b06b..bd1f21e 100644 (file)
@@ -30,5 +30,9 @@ export class CircEventsComponent extends DialogComponent implements OnInit {
                 .then(str => this.modeLabel = str);
         });
     }
+
+    isArray(target: any): boolean {
+        return Array.isArray(target);
+    }
 }
 
index 6b402bb..cd22019 100644 (file)
@@ -30,6 +30,7 @@ import {MarkDamagedDialogComponent
 import {MarkMissingDialogComponent
     } from '@eg/staff/share/holdings/mark-missing-dialog.component';
 import {ClaimsReturnedDialogComponent} from './claims-returned-dialog.component';
+import {ToastService} from '@eg/share/toast/toast.service';
 
 export interface CircGridEntry {
     index: string; // class + id -- row index
@@ -122,6 +123,7 @@ export class CircGridComponent implements OnInit {
         private audio: AudioService,
         private store: StoreService,
         private printer: PrintService,
+        private toast: ToastService,
         private serverStore: ServerStoreService
     ) {}
 
@@ -149,6 +151,17 @@ export class CircGridComponent implements OnInit {
                 return 'less-intense-alert';
             }
         };
+
+        this.serverStore.getItemBatch(['ui.circ.suppress_checkin_popups'])
+        .then(sets => {
+            this.circ.suppressCheckinPopups =
+                sets['ui.circ.suppress_checkin_popups'];
+        });
+    }
+
+    reportError(err: any) {
+        console.error('Circ error occurred: ' + err);
+        this.toast.danger(err); // EgEvent has a toString()
     }
 
     // Ask the caller to update our data set.
@@ -437,7 +450,7 @@ export class CircGridComponent implements OnInit {
                 // Value can be null when dialogs are canceled
                 if (result) { refreshNeeded = true; }
             },
-            err => console.error(err),
+            err => this.reportError(err),
             () => {
                 dialog.close();
                 if (refreshNeeded) {
@@ -463,7 +476,7 @@ export class CircGridComponent implements OnInit {
                     if (resp.success) { refreshNeeded = true; }
                     dialog.increment();
                 },
-                err => console.error(err),
+                err => this.reportError(err),
                 () => {
                     dialog.close();
                     if (refreshNeeded) {
@@ -481,13 +494,17 @@ export class CircGridComponent implements OnInit {
 
         const dialog = this.openProgressDialog(rows);
 
+        let changesApplied = false;
         return this.circ.checkinBatch(this.getCopyIds(rows), params)
         .pipe(tap(
-            result => dialog.increment(),
-            err => null,
+            result => {
+                if (result) { changesApplied = true; }
+                dialog.increment();
+            },
+            err => this.reportError(err),
             () => {
                 dialog.close();
-                if (!noReload) { this.emitReloadRequest(); }
+                if (changesApplied && !noReload) { this.emitReloadRequest(); }
             }
         ));
     }
@@ -504,7 +521,7 @@ export class CircGridComponent implements OnInit {
             );
         })).subscribe(
             result => dialog.increment(),
-            err => console.error(err),
+            err => this.reportError(err),
             () => {
                 dialog.close();
                 this.emitReloadRequest();
@@ -542,7 +559,7 @@ export class CircGridComponent implements OnInit {
                 this.getCopyIds(rows), {claims_never_checked_out: true}
             ).subscribe(
                 result => dialog.increment(),
-                err => console.error(err),
+                err => this.reportError(err),
                 () => {
                     dialog.close();
                     this.emitReloadRequest();
index bccc56b..f75803f 100644 (file)
@@ -330,5 +330,5 @@ input.small {
  */
 .less-intense-alert {
   background-color: #f9dede;
-  color: black;
+  color: #212121;
 }