LP1904036 checkin UI
authorBill Erickson <berickxx@gmail.com>
Fri, 9 Apr 2021 19:38:57 +0000 (15:38 -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/checkin/checkin.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/checkin/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts
Open-ILS/src/eg2/src/app/staff/nav.component.html
Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts
Open-ILS/src/eg2/src/app/staff/share/circ/route-dialog.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
new file mode 100644 (file)
index 0000000..0ad28c6
--- /dev/null
@@ -0,0 +1,107 @@
+<eg-staff-banner i18n-bannerText bannerText="Checkin Items"></eg-staff-banner>
+<eg-circ-components></eg-circ-components>
+<eg-progress-dialog #progressDialog></eg-progress-dialog>
+<eg-barcode-select #barcodeSelect></eg-barcode-select>
+<eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
+
+<div class="row mb-3 pb-3 border-bottom">
+  <div class="col-lg-12 d-flex">
+    <div class="form-inline">
+      <div class="input-group">
+        <div class="input-group-prepend">
+          <span class="input-group-text" i18n>Barcode</span>
+        </div>
+        <input type="text" class="form-control" id="barcode-input"
+          placeholder="Barcode..." i18n-placeholder
+          [(ngModel)]="barcode" [disabled]="checkinNoncat != null"
+          i18n-aria-label aria-label="Barcode Input" (keydown.enter)="checkin()" />
+        <div class="input-group-append">
+          <button class="btn btn-outline-dark" (keydown.enter)="checkin()" 
+            (click)="checkin()" i18n>Submit</button>
+        </div>
+      </div>
+    </div>
+    <div class="flex-1"></div>
+    <div>
+      <span class="mr-2" i18n>Effective Date:</span>
+      <eg-date-select [initialIso]="effectiveDate" 
+        (onChangeAsIso)="setEffectiveDate($event)"></eg-date-select>
+    </div>
+  </div>
+</div>
+
+<div *ngIf="fineTally > 0">
+  <span class="mr-2" i18n>Fine Tally: </span>
+  <span class="badge badge-danger">{{fineTally | currency}}</span>
+</div>
+
+<!-- doc_id below because checkin returns an MVR -->
+<ng-template #titleTemplate let-r="row">
+  <ng-container *ngIf="r.record">
+    <a routerLink="/staff/catalog/record/{{r.record.doc_id()}}">{{r.title}}</a>
+  </ng-container>
+  <ng-container *ngIf="!r.record">{{r.title}}</ng-container>
+</ng-template>
+
+<div class="row">
+  <div class="col-lg-12">
+    <eg-grid #grid [dataSource]="gridDataSource" [sortable]="true"
+      [useLocalSort]="true" [cellTextGenerator]="cellTextGenerator"
+      [disablePaging]="true" persistKey="circ.checkin">
+
+      <!--
+                       <eg-grid-toolbar-action
+                               i18n-group group="Add" i18n-label label="Add Item Alerts"
+                               (onClick)="openItemAlerts($event, 'create')">
+                       </eg-grid-toolbar-action>
+
+                       <eg-grid-toolbar-action
+                               i18n-group group="Add" i18n-label label="Manage Item Alerts"
+        [disabled]="checkinsGrid.context.rowSelector.selected().length !== 1"
+                               (onClick)="openItemAlerts($event, 'manage')">
+                       </eg-grid-toolbar-action>
+      -->
+
+      <eg-grid-column path="index" [index]="true" 
+        label="Row Index" i18n-label [hidden]="true"></eg-grid-column>
+
+      <eg-grid-column path="mbts.balance_owed" label="Balance Owed" 
+        datatype="money" i18n-label></eg-grid-column>
+
+      <eg-grid-column path="copy.barcode" label="Barcode" i18n-label>
+      </eg-grid-column>
+
+      <eg-grid-column path="circ.id" label="Bill #" i18n-label>
+      </eg-grid-column>
+
+      <eg-grid-column path="circ.checkin_time" label="Checkin Date" i18n-label
+        datatype="timestamp" [datePlusTime]="true"></eg-grid-column>
+
+      <eg-grid-column path="patron.family_name" label="Family Name" i18n-label>
+      </eg-grid-column>
+
+      <eg-grid-column path="circ.xact_finish" label="Finish" i18n-label
+        datatype="timestamp" [datePlusTime]="true"></eg-grid-column>
+
+      <eg-grid-column path="copy.location.name" label="Location" i18n-label>
+      </eg-grid-column>
+
+      <eg-grid-column name="routeTo" label="Route To" i18n-label>
+      </eg-grid-column>
+
+      <eg-grid-column path="circ.xact_start" label="Start" i18n-label
+        datatype="timestamp" [datePlusTime]="true"></eg-grid-column>
+
+      <eg-grid-column path="title" label="Title" i18n-label 
+        [cellTemplate]="titleTemplate"></eg-grid-column>
+
+      <eg-grid-column path="copy.circ_modifier" 
+        label="Circulation Modifier" i18n-label></eg-grid-column>
+
+      <eg-grid-column path="copy.circ_lib.shortname"
+        label="Circulation Library" i18n-label></eg-grid-column>
+
+    </eg-grid>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.component.ts
new file mode 100644 (file)
index 0000000..6ad1c8f
--- /dev/null
@@ -0,0 +1,156 @@
+import {Component, ViewChild, OnInit, AfterViewInit, HostListener} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {from} from 'rxjs';
+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 {CircService, CircDisplayInfo, CheckinParams, CheckinResult
+    } from '@eg/staff/share/circ/circ.service';
+import {Pager} from '@eg/share/util/pager';
+import {BarcodeSelectComponent
+    } from '@eg/staff/share/barcodes/barcode-select.component';
+
+interface CheckinGridEntry extends CheckinResult {
+    title?: string;
+    author?: string;
+    isbn?: string;
+}
+
+@Component({
+  templateUrl: 'checkin.component.html'
+})
+export class CheckinComponent implements OnInit, AfterViewInit {
+    checkins: CheckinGridEntry[] = [];
+    autoIndex = 0;
+
+    barcode: string;
+    backdate: string; // ISO
+    fineTally = 0;
+
+    gridDataSource: GridDataSource = new GridDataSource();
+    cellTextGenerator: GridCellTextGenerator;
+
+    private copiesInFlight: {[barcode: string]: boolean} = {};
+
+    @ViewChild('grid') private grid: GridComponent;
+    @ViewChild('barcodeSelect') private barcodeSelect: BarcodeSelectComponent;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private net: NetService,
+        private org: OrgService,
+        private auth: AuthService,
+        private store: ServerStoreService,
+        private circ: CircService,
+        public patronService: PatronService
+    ) {}
+
+    ngOnInit() {
+        this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+            return from(this.checkins);
+        };
+    }
+
+    ngAfterViewInit() {
+        this.focusInput();
+    }
+
+    focusInput() {
+        const input = document.getElementById('barcode-input');
+        if (input) { input.focus(); }
+    }
+
+    checkin(params?: CheckinParams, override?: boolean): Promise<CheckinResult> {
+        if (!this.barcode) { return Promise.resolve(null); }
+
+        const promise = params ? Promise.resolve(params) : this.collectParams();
+
+        return promise.then((collectedParams: CheckinParams) => {
+            if (!collectedParams) { return null; }
+
+            if (this.copiesInFlight[this.barcode]) {
+                console.debug('Item ' + this.barcode + ' is already mid-checkin');
+                return null;
+            }
+
+            this.copiesInFlight[this.barcode] = true;
+            return this.circ.checkin(collectedParams);
+        })
+
+        .then((result: CheckinResult) => {
+            if (result) {
+                this.dispatchResult(result);
+                return result;
+            }
+        })
+
+        .finally(() => delete this.copiesInFlight[this.barcode]);
+    }
+
+    dispatchResult(result: CheckinResult) {
+        if (result.success) {
+            this.gridifyResult(result);
+            this.resetForm();
+            return;
+        }
+    }
+
+    collectParams(): Promise<CheckinParams> {
+
+        const params: CheckinParams = {
+            copy_barcode: this.barcode,
+            backdate: this.backdate
+        };
+
+        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: CheckinResult) {
+        const entry: CheckinGridEntry = result;
+        entry.index = this.autoIndex++;
+
+        if (result.record) {
+            entry.title = result.record.title();
+            entry.author = result.record.author();
+            entry.isbn = result.record.isbn();
+
+        } else if (result.copy) {
+            entry.title = result.copy.dummy_title();
+            entry.author = result.copy.dummy_author();
+            entry.isbn = result.copy.dummy_isbn();
+        }
+
+        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.checkins.unshift(entry);
+        this.grid.reload();
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.module.ts
new file mode 100644 (file)
index 0000000..2455fa0
--- /dev/null
@@ -0,0 +1,35 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {CheckinRoutingModule} 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 {CheckinComponent} from './checkin.component';
+
+@NgModule({
+  declarations: [
+    CheckinComponent
+  ],
+  imports: [
+    StaffCommonModule,
+    CheckinRoutingModule,
+    FmRecordEditorModule,
+    BillingModule,
+    CircModule,
+    HoldsModule,
+    HoldingsModule,
+    BookingModule,
+    PatronModule,
+    BarcodesModule
+  ],
+  providers: [
+  ]
+})
+
+export class CheckinModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/checkin/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/checkin/routing.module.ts
new file mode 100644 (file)
index 0000000..df175a0
--- /dev/null
@@ -0,0 +1,15 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {CheckinComponent} from './checkin.component';
+
+const routes: Routes = [{
+    path: '',
+    component: CheckinComponent
+}];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class CheckinRoutingModule {}
index 54e0ae4..5122ec2 100644 (file)
@@ -13,6 +13,10 @@ const routes: Routes = [{
   path: 'holds',
   loadChildren: () =>
     import('./holds/holds.module').then(m => m.HoldsUiModule)
+}, {
+  path: 'checkin',
+  loadChildren: () =>
+    import('./checkin/checkin.module').then(m => m.CheckinModule)
 }];
 
 @NgModule({
index 24b027f..ec48cbc 100644 (file)
             <span class="material-icons" aria-hidden="true">trending_down</span>
             <span i18n>Check In</span>
           </a>
+          <a class="dropdown-item" routerLink="/staff/circ/checkin"
+            egAccessKey keyCtx="navbar" i18n-keySpec i18n-keyDesc
+            keySpec="alt+f2" keyDesc="Checkin">
+            <span class="material-icons" aria-hidden="true">trending_down</span>
+            <span i18n>Check In (Experimental)</span>
+          </a>
           <a class="dropdown-item" href="/eg/staff/circ/checkin/capture"
             egAccessKey keyCtx="navbar" i18n-keySpec i18n-keyDesc
             keySpec="shift+f2" keyDesc="Capture Holds">
index 619e31f..c8a31c1 100644 (file)
@@ -149,6 +149,7 @@ export interface CheckinParams {
     claims_never_checked_out?: boolean;
     void_overdues?: boolean;
     auto_print_hold_transits?: boolean;
+    backdate?: string;
 
     // internal tracking
     _override?: boolean;
@@ -163,11 +164,14 @@ export interface CheckinResult {
     copy?: IdlObject;
     volume?: IdlObject;
     circ?: IdlObject;
+    parent_circ?: IdlObject;
+    mbts?: IdlObject;
     record?: IdlObject;
     hold?: IdlObject;
     transit?: IdlObject;
     org?: number;
     patron?: IdlObject;
+    routeTo?: string;
 }
 
 @Injectable()
@@ -206,10 +210,13 @@ export class CircService {
     // 'circ' is fleshed with copy, vol, bib, wide_display_entry
     // Extracts some display info from a fleshed circ.
     getDisplayInfo(circ: IdlObject): CircDisplayInfo {
+        return this.getCopyDisplayInfo(circ.target_copy());
+    }
 
-        const copy = circ.target_copy();
+    getCopyDisplayInfo(copy: IdlObject): CircDisplayInfo {
 
-        if (copy.call_number().id() === -1) { // precat
+        if (copy.call_number() === -1 || copy.call_number().id() === -1) {
+            // Precat Copy
             return {
                 title: copy.dummy_title(),
                 author: copy.dummy_author(),
@@ -273,6 +280,7 @@ export class CircService {
         ).toPromise().then(transit => {
             transit.source(this.org.get(transit.source()));
             transit.dest(this.org.get(transit.dest()));
+            result.routeTo = transit.dest().shortname();
             return transit;
         });
     }
@@ -541,15 +549,20 @@ export class CircService {
             params: params,
             success: success,
             circ: payload.circ,
+            parent_circ: payload.parent_circ,
             copy: payload.copy,
             volume: payload.volume,
             record: payload.record,
             transit: payload.transit
         };
 
-        let promise = Promise.resolve();;
         const copy = result.copy;
         const volume = result.volume;
+        const transit = result.transit;
+        const circ = result.circ;
+        const parent_circ = result.parent_circ;
+
+        let promise = Promise.resolve();;
 
         if (copy) {
             if (this.copyLocationCache[copy.location()]) {
@@ -579,6 +592,25 @@ export class CircService {
             }
         }
 
+        if (transit) {
+            if (typeof transit.dest() !== 'object') {
+                transit.dest(this.org.get(transit.dest()));
+            }
+            if (typeof transit.source() !== 'object') {
+                transit.source(this.org.get(transit.source()));
+            }
+        }
+
+        // for checkin, the mbts lives on the main circ
+        if (circ && circ.billable_transaction()) {
+            result.mbts = circ.billable_transaction().summary();
+        }
+
+        // on renewals, the mbts lives on the parent circ
+        if (parent_circ && parent_circ.billable_transaction()) {
+            result.mbts = parent_circ.billable_transaction().summary();
+        }
+
         return promise.then(_ => result);
     }
 
@@ -598,7 +630,6 @@ export class CircService {
             return this.checkin(params);
         }
 
-
         // Alerts that require a manual override.
         if (allEvents.filter(
             e => CAN_OVERRIDE_CHECKIN_ALERTS.includes(e.textcode)).length > 0) {
@@ -616,12 +647,14 @@ export class CircService {
 
             case 'ITEM_NOT_CATALOGED':
                 this.audio.play('error.checkout.no_cataloged');
+                result.routeTo = 'Cataloging'; // TODO
 
                 if (!this.suppressCheckinPopups && !this.ignoreCheckinPrecats) {
                     // Tell the user its a precat and return the result.
                     return this.components.routeToCatalogingDialog.open()
                     .toPromise().then(_ => result);
                 }
+                break;
 
             case 'ROUTE_ITEM':
                 this.components.routeDialog.checkin = result;
@@ -651,6 +684,7 @@ export class CircService {
                 if (hold) {
 
                     if (hold.pickup_lib() === this.auth.user().ws_ou()) {
+                        result.routeTo = 'Holds Shelf'; // TODO
                         this.components.routeDialog.checkin = result;
                         return this.components.routeDialog.open().toPromise()
                         .then(_ => result);
@@ -661,8 +695,13 @@ export class CircService {
                     }
 
                 } else {
-                    console.warn("API Returned insufficient info on holds");
+                    console.warn('API Returned insufficient info on holds');
                 }
+
+            case 11: /* CATALOGING */
+                this.audio.play('info.checkin.cataloging');
+                result.routeTo = 'Cataloging'; // TODO
+                // TODO more...
         }
 
         return Promise.resolve(result);
index 94da527..7972bed 100644 (file)
@@ -41,7 +41,6 @@ export class RouteDialogComponent extends DialogComponent {
     }
 
     open(ops?: NgbModalOptions): Observable<any> {
-
         // Depending on various settings, the dialog may never open.
         // But in some cases we still have to collect the data
         // for printing.
@@ -66,7 +65,6 @@ export class RouteDialogComponent extends DialogComponent {
     }
 
     collectData(): Promise<boolean> {
-
         let promise = Promise.resolve(null);
         const hold = this.checkin.hold;