LP1904036 checkin; route dialogs
authorBill Erickson <berickxx@gmail.com>
Fri, 9 Apr 2021 16:33:53 +0000 (12:33 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:30 +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.module.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/route-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/circ/route-dialog.component.ts [new file with mode: 0644]

index db2787c..2bb8563 100644 (file)
@@ -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<Promise<any[]>> {
 
     constructor(
         private store: ServerStoreService,
-        private context: PatronContextService
+        private context: PatronContextService,
+        private circ: CircService
     ) {}
 
     resolve(
@@ -130,6 +131,7 @@ export class PatronResolver implements Resolve<Promise<any[]>> {
           'ui.admin.patron_log.max_entries'
         ]).then(settings => {
             this.context.settingsCache = settings;
+            return this.circ.applySettings();
         });
     }
 }
index dc639e9..189d27a 100644 (file)
@@ -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: [
index eb9e5ba..e347c1a 100644 (file)
@@ -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<any> {
+        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<IdlObject> {
+        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<IdlObject> {
+        // 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<IdlObject[]> {
 
         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);
index c287856..5c69ceb 100644 (file)
@@ -8,11 +8,13 @@
   i18n-dialogBody dialogBody="This item needs to be routed to CATALOGING">
 </eg-alert-dialog>
 
-<eg-string key="staff.circ.checkin.location.alert"
-  i18n-text text="Item {{barcode}} needs to be routed to {{location}}">
+<ng-template #locAlertTemplate let-barcode="barcode" let-location="location" i18n>
+  Item {{barcode}} needs to be routed to {{location}}.
+</ng-template>
+<!-- dialogBody is generated dynamically from string above -->
+<eg-string key="staff.circ.checkin.location.alert" [template]="locAlertTemplate">
 </eg-string>
 
-<!-- dialogBody is generated dynamically from string above -->
 
 <eg-alert-dialog #locationAlertDialog i18n-dialogTitle dialogTitle="Route Item">
 </eg-alert-dialog>
index d1bcadb..2550ea7 100644 (file)
@@ -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 (file)
index 0000000..1850598
--- /dev/null
@@ -0,0 +1,15 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 i18n>Route Item to Holds Shelf</h4>
+    <button type="button" class="close"
+      i18n-aria-label aria-label="Close" (click)="close()">
+      <span aria-hidden="true">&times;</span>
+    </button>
+  </div>
+  <div class="modal-body">
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-success" (click)="close()" i18n>Submit</button>
+    <button type="button" class="btn btn-warning" (click)="close()" i18n>Cancel</button>
+  </div>
+</ng-template>
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 (file)
index 0000000..d488a51
--- /dev/null
@@ -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<any> {
+
+        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<boolean> {
+
+        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<boolean> {
+
+        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<any> {
+        return null;
+    }
+}
+