<div class="col-lg-12">
<eg-grid #checkoutsGrid [dataSource]="gridDataSource" [sortable]="true"
[useLocalSort]="true" [cellTextGenerator]="cellTextGenerator"
- persistKey="circ.patron.checkout">
+ [disablePaging]="true" persistKey="circ.patron.checkout">
<eg-grid-toolbar-action
i18n-group group="Add" i18n-label label="Add Item Alerts"
templateUrl: 'checkout.component.html',
selector: 'eg-patron-checkout'
})
-export class CheckoutComponent implements OnInit {
+export class CheckoutComponent implements OnInit, AfterViewInit {
maxNoncats = 99; // Matches AngJS version
checkoutNoncat: IdlObject = null;
let barcode;
const promise = params ? Promise.resolve(params) : this.collectParams();
- return promise.then((params: CheckoutParams) => {
- if (!params) { return null; }
+ return promise.then((collectedParams: CheckoutParams) => {
+ if (!collectedParams) { return null; }
- barcode = params.copy_barcode || '';
+ barcode = collectedParams.copy_barcode || '';
if (barcode) {
this.copiesInFlight[barcode] = true;
}
- return this.circ.checkout(params);
+ return this.circ.checkout(collectedParams);
})
.then((result: CheckoutResult) => {
Object.keys(values).forEach(key => params[key] = values[key]);
this.checkout(params);
}
- })
+ });
}
selectedCopyIds(rows: CircGridEntry[]): number[] {
--- /dev/null
+<ng-template #progress>
+ <div class="row">
+ <div class="col-lg-6 offset-lg-3">
+ <eg-progress-inline></eg-progress-inline>
+ </div>
+ </div>
+</ng-template>
+
+<div>
+ <ul ngbNav #itemsNav="ngbNav" class="nav-tabs"
+ [activeId]="itemsTab" (navChange)="tabChange($event)">
+ <li ngbNavItem="checkouts">
+ <a ngbNavLink i18n>Items Checked Out ({{mainList.length}})</a>
+ <ng-template ngbNavContent>
+ <ng-container *ngIf="loading">
+ <ng-container *ngTemplateOutlet="progress"></ng-container>
+ </ng-container>
+ <eg-circ-grid #checkoutsGrid></eg-circ-grid>
+ </ng-template>
+ </li>
+ <li ngbNavItem="other">
+ <a ngbNavLink i18n>Other/Special Circulations ({{altList.length}})</a>
+ <ng-template ngbNavContent>
+ <ng-container>
+ </ng-container>
+ </ng-template>
+ </li>
+ <li ngbNavItem="noncat">
+ <a ngbNavLink i18n>Non-Cataloged Circulations</a>
+ <ng-template ngbNavContent>
+ <ng-container>
+ </ng-container>
+ </ng-template>
+ </li>
+ </ul>
+ <div [ngbNavOutlet]="itemsNav"></div>
+</div>
+
--- /dev/null
+import {Component, OnInit, AfterViewInit, Input, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Observable, empty, of, from} from 'rxjs';
+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 {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PatronService} from '@eg/staff/share/patron/patron.service';
+import {PatronManagerService} from './patron.service';
+import {CheckoutResult, CircService} from '@eg/staff/share/circ/circ.service';
+import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
+import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {StoreService} from '@eg/core/store.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {AudioService} from '@eg/share/util/audio.service';
+import {CopyAlertsDialogComponent
+ } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {CircGridComponent} from '@eg/staff/share/circ/grid.component';
+
+@Component({
+ templateUrl: 'items.component.html',
+ selector: 'eg-patron-items'
+})
+export class ItemsComponent implements OnInit, AfterViewInit {
+
+ // Note we can get the patron id from this.context.patron.id(), but
+ // on a new page load, this requires us to wait for the arrival of
+ // the patron object before we can fetch our circs. This is just simpler.
+ @Input() patronId: number;
+
+ itemsTab = 'checkouts';
+ loading = false;
+ mainList: number[] = [];
+ altList: number[] = [];
+ noncatDataSource: GridDataSource = new GridDataSource();
+
+ @ViewChild('checkoutsGrid') private checkoutsGrid: CircGridComponent;
+
+ constructor(
+ private org: OrgService,
+ private net: NetService,
+ private auth: AuthService,
+ public circ: CircService,
+ private audio: AudioService,
+ private store: StoreService,
+ private serverStore: ServerStoreService,
+ public patronService: PatronService,
+ public context: PatronManagerService
+ ) {}
+
+ ngOnInit() {
+ }
+
+ ngAfterViewInit() {
+ setTimeout(() => this.loadTab(this.itemsTab));
+ }
+
+ tabChange(evt: NgbNavChangeEvent) {
+ setTimeout(() => this.loadTab(evt.nextId));
+ }
+
+ loadTab(name: string) {
+ this.loading = true;
+ let promise;
+ if (name === 'checkouts') {
+ promise = this.loadCheckoutsGrid();
+ }
+
+ promise.then(_ => this.loading = false);
+ }
+
+ loadCheckoutsGrid(): Promise<any> {
+ this.mainList = [];
+ this.altList = [];
+
+ const promise = this.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.user.checked_out.authoritative',
+ this.auth.token(), this.patronId
+ ).toPromise().then(checkouts => {
+ this.mainList = checkouts.overdue.concat(checkouts.out);
+
+ // TODO promise_circs, etc.
+ });
+
+ // TODO: fetch checked in
+
+ return promise.then(_ => {
+ this.checkoutsGrid.load(this.mainList)
+ .subscribe(null, null, () => this.checkoutsGrid.reloadGrid());
+ });
+ }
+
+ /*
+ function get_circ_ids() {
+ $scope.main_list = [];
+ $scope.alt_list = [];
+
+ // we can fetch these in parallel
+ var promise1 = egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.user.checked_out.authoritative',
+ egCore.auth.token(), $scope.patron_id
+ ).then(function(outs) {
+ $scope.main_list = outs.overdue.concat(outs.out);
+ promote_circs(outs.lost, display_lost, true);
+ promote_circs(outs.long_overdue, display_lo, true);
+ promote_circs(outs.claims_returned, display_cr, true);
+ });
+
+ // only fetched checked-in-with-bills circs if configured to display
+ var promise2 = !fetch_checked_in ? $q.when() : egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.user.checked_in_with_fines.authoritative',
+ egCore.auth.token(), $scope.patron_id
+ ).then(function(outs) {
+ promote_circs(outs.lost, display_lost);
+ promote_circs(outs.long_overdue, display_lo);
+ promote_circs(outs.claims_returned, display_cr);
+ });
+
+ return $q.all([promise1, promise2]);
+ }
+ */
+
+}
+
+
<a ngbNavLink i18n>Items Out</a>
<ng-template ngbNavContent>
<div class="">
+ <eg-patron-items [patronId]="patronId"></eg-patron-items>
</div>
</ng-template>
</li>
import {BcSearchComponent} from './bcsearch.component';
import {PrecatCheckoutDialogComponent} from './precat-dialog.component';
import {BarcodesModule} from '@eg/staff/share/barcodes/barcodes.module';
+import {ItemsComponent} from './items.component';
@NgModule({
declarations: [
EditComponent,
EditToolbarComponent,
BcSearchComponent,
+ ItemsComponent,
PrecatCheckoutDialogComponent
],
imports: [
}
selectionChanged() {
- const id = Object.keys(this.inputs).map(id => Number(id))
- .filter(id => this.inputs[id] === true)[0];
+ const id = Object.keys(this.inputs).map(i => Number(i))
+ .filter(i => this.inputs[i] === true)[0];
if (id) {
this.selected = this.matches.filter(match => match.id === id)[0];
if (!results) { return result; }
- results.forEach(result => {
- if (!this.evt.parse(result)) {
- this.matches.push(result);
+ results.forEach(res => {
+ if (!this.evt.parse(res)) {
+ this.matches.push(res);
}
});
import {StaffCommonModule} from '@eg/staff/common.module';
import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
import {CircService} from './circ.service';
+import {CircGridComponent} from './grid.component';
@NgModule({
declarations: [
+ CircGridComponent
],
imports: [
StaffCommonModule,
HoldingsModule
],
exports: [
+ CircGridComponent
],
providers: [
CircService
export interface CheckoutResult {
index: number;
evt: EgEvent;
- params: CheckoutParams,
+ params: CheckoutParams;
success: boolean;
copy?: IdlObject;
circ?: IdlObject;
--- /dev/null
+
+<eg-progress-dialog #progressDialog></eg-progress-dialog>
+<eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
+
+<ng-template #titleTemplate let-r="row">
+ <ng-container *ngIf="r.record">
+ <a routerLink="/staff/catalog/record/{{r.record.id()}}">{{r.title}}</a>
+ </ng-container>
+ <ng-container *ngIf="!r.record">{{r.title}}</ng-container>
+</ng-template>
+
+<eg-grid #circGrid [dataSource]="gridDataSource" [sortable]="true"
+ [useLocalSort]="true" [cellTextGenerator]="cellTextGenerator"
+ [disablePaging]="true" [persistKey]="persistKey">
+
+ <eg-grid-toolbar-action
+ i18n-group group="Add" i18n-label label="Add Item Alerts"
+ (onClick)="openItemAlerts($event, 'create')">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-column [index]="true" path="circ.id"
+ label="Circ ID" i18n-label></eg-grid-column>
+
+ <!-- TODO
+ [datePlusTime]="true" when non-full-day circ
+ -->
+ <eg-grid-column path="dueDate" label="Due Date" i18n-label
+ datatype="timestamp"></eg-grid-column>
+
+ <eg-grid-column path="copy.barcode" label="Barcode" i18n-label></eg-grid-column>
+
+ <eg-grid-column path="title" label="Title" i18n-label
+ [cellTemplate]="titleTemplate"></eg-grid-column>
+
+ <eg-grid-column path="nonCatCount" label="Non-Cataloged Count"
+ i18n-label></eg-grid-column>
+
+</eg-grid>
+
+
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Observable, empty, of, from} from 'rxjs';
+import {map, tap, switchMap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {CheckoutParams, CheckoutResult, CircService} from './circ.service';
+import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
+import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {Pager} from '@eg/share/util/pager';
+import {StoreService} from '@eg/core/store.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {AudioService} from '@eg/share/util/audio.service';
+import {CopyAlertsDialogComponent
+ } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {ArrayUtil} from '@eg/share/util/array';
+
+export interface CircGridEntry {
+ title?: string;
+ author?: string;
+ isbn?: string;
+ copy?: IdlObject;
+ circ?: IdlObject;
+ dueDate?: string;
+ copyAlertCount?: number;
+ nonCatCount?: number;
+}
+
+const CIRC_FLESH_DEPTH = 4;
+const CIRC_FLESH_FIELDS = {
+ circ: ['target_copy', 'workstation', 'checkin_workstation'],
+ acp: [
+ 'call_number',
+ 'holds_count',
+ 'status',
+ 'circ_lib',
+ 'location',
+ 'floating',
+ 'age_protect',
+ 'parts'
+ ],
+ acpm: ['part'],
+ acn: ['record', 'owning_lib', 'prefix', 'suffix'],
+ bre: ['wide_display_entry']
+};
+
+@Component({
+ templateUrl: 'grid.component.html',
+ selector: 'eg-circ-grid'
+})
+export class CircGridComponent implements OnInit {
+
+ @Input() persistKey: string;
+
+ entries: CircGridEntry[] = null;
+ gridDataSource: GridDataSource = new GridDataSource();
+ cellTextGenerator: GridCellTextGenerator;
+
+ @ViewChild('circGrid') private circGrid: GridComponent;
+ @ViewChild('copyAlertsDialog')
+ private copyAlertsDialog: CopyAlertsDialogComponent;
+
+ constructor(
+ private org: OrgService,
+ private net: NetService,
+ private pcrud: PcrudService,
+ public circ: CircService,
+ private audio: AudioService,
+ private store: StoreService,
+ private serverStore: ServerStoreService
+ ) {}
+
+ ngOnInit() {
+
+ // The grid never fetches data directly.
+ // The caller is responsible initiating all data loads.
+ this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
+ if (this.entries) {
+ return from(this.entries);
+ } else {
+ return empty();
+ }
+ };
+
+ this.cellTextGenerator = {
+ title: row => row.title
+ };
+ }
+
+ // Reload the grid without any data retrieval
+ reloadGrid() {
+ this.circGrid.reload();
+ }
+
+ // Fetch circulation data and make it available to the grid.
+ load(circIds: number[]): Observable<CircGridEntry> {
+
+ // No circs to load
+ if (!circIds || circIds.length === 0) { return empty(); }
+
+ // Return the circs we have already retrieved.
+ if (this.entries) { return from(this.entries); }
+
+ this.entries = [];
+
+ return this.pcrud.search('circ', {id: circIds}, {
+ flesh: CIRC_FLESH_DEPTH,
+ flesh_fields: CIRC_FLESH_FIELDS,
+ order_by : {circ : ['xact_start']},
+
+ // Avoid fetching the MARC blob by specifying which
+ // fields on the bre to select. More may be needed.
+ // Note that fleshed fields are explicitly selected.
+ select: {bre : ['id']}
+
+ }).pipe(map(circ => {
+
+ const entry = this.gridify(circ);
+ this.entries.push(entry);
+ return entry;
+ }));
+ }
+
+ gridify(circ: IdlObject): CircGridEntry {
+
+ const entry: CircGridEntry = {
+ circ: circ,
+ dueDate: circ.due_date(),
+ copyAlertCount: 0 // TODO
+ };
+
+ const copy = circ.target_copy();
+ entry.copy = copy;
+
+ // Some values have to be manually extracted / normalized
+ if (copy.call_number().id() === -1) {
+
+ entry.title = copy.dummy_title();
+ entry.author = copy.dummy_author();
+ entry.isbn = copy.dummy_isbn();
+
+ } else {
+
+ const display =
+ copy.call_number().record().wide_display_entry();
+
+ entry.title = display.title();
+ entry.author = display.author();
+ entry.isbn = display.isbn();
+ }
+
+ return entry;
+ }
+
+ selectedCopyIds(rows: CircGridEntry[]): number[] {
+ return rows
+ .filter(row => row.copy)
+ .map(row => Number(row.copy.id()));
+ }
+
+ openItemAlerts(rows: CircGridEntry[], mode: string) {
+ const copyIds = this.selectedCopyIds(rows);
+ if (copyIds.length === 0) { return; }
+
+ this.copyAlertsDialog.copyIds = copyIds;
+ this.copyAlertsDialog.mode = mode;
+ this.copyAlertsDialog.open({size: 'lg'}).subscribe(
+ modified => {
+ if (modified) {
+ // TODO: verify the modiifed alerts are present
+ // or go fetch them.
+ this.circGrid.reload();
+ }
+ }
+ );
+ }
+}
+