--- /dev/null
+<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>
+
--- /dev/null
+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();
+ }
+}
+
--- /dev/null
+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 {}
+
--- /dev/null
+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 {}
path: 'holds',
loadChildren: () =>
import('./holds/holds.module').then(m => m.HoldsUiModule)
+}, {
+ path: 'checkin',
+ loadChildren: () =>
+ import('./checkin/checkin.module').then(m => m.CheckinModule)
}];
@NgModule({
<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">
claims_never_checked_out?: boolean;
void_overdues?: boolean;
auto_print_hold_transits?: boolean;
+ backdate?: string;
// internal tracking
_override?: boolean;
copy?: IdlObject;
volume?: IdlObject;
circ?: IdlObject;
+ parent_circ?: IdlObject;
+ mbts?: IdlObject;
record?: IdlObject;
hold?: IdlObject;
transit?: IdlObject;
org?: number;
patron?: IdlObject;
+ routeTo?: string;
}
@Injectable()
// '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(),
).toPromise().then(transit => {
transit.source(this.org.get(transit.source()));
transit.dest(this.org.get(transit.dest()));
+ result.routeTo = transit.dest().shortname();
return transit;
});
}
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()]) {
}
}
+ 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);
}
return this.checkin(params);
}
-
// Alerts that require a manual override.
if (allEvents.filter(
e => CAN_OVERRIDE_CHECKIN_ALERTS.includes(e.textcode)).length > 0) {
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;
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);
}
} 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);
}
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.
}
collectData(): Promise<boolean> {
-
let promise = Promise.resolve(null);
const hold = this.checkin.hold;