From: Bill Erickson Date: Fri, 19 Feb 2021 21:21:07 +0000 (-0500) Subject: LP1904036 Patron items out grid; shared circ grid X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=5003b599d292fcd71ff07c1116f5a623ded6d6cf;p=evergreen%2Fpines.git LP1904036 Patron items out grid; shared circ grid Signed-off-by: Bill Erickson Signed-off-by: Jane Sandberg Signed-off-by: Galen Charlton --- diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html index 2e73da29ba..b1e12fe755 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.html @@ -84,7 +84,7 @@
+ [disablePaging]="true" persistKey="circ.patron.checkout"> { - if (!params) { return null; } + return promise.then((collectedParams: CheckoutParams) => { + if (!collectedParams) { return null; } - barcode = params.copy_barcode || ''; + barcode = collectedParams.copy_barcode || ''; if (barcode) { @@ -144,7 +144,7 @@ export class CheckoutComponent implements OnInit { this.copiesInFlight[barcode] = true; } - return this.circ.checkout(params); + return this.circ.checkout(collectedParams); }) .then((result: CheckoutResult) => { @@ -287,7 +287,7 @@ export class CheckoutComponent implements OnInit { Object.keys(values).forEach(key => params[key] = values[key]); this.checkout(params); } - }) + }); } selectedCopyIds(rows: CircGridEntry[]): number[] { diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.html new file mode 100644 index 0000000000..5ec8605436 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.html @@ -0,0 +1,38 @@ + +
+
+ +
+
+
+ + + diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.ts new file mode 100644 index 0000000000..4886c0df9e --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/items.component.ts @@ -0,0 +1,131 @@ +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 { + 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]); + } + */ + +} + + diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.html index b520060fde..736206979a 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.html +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.html @@ -57,6 +57,7 @@ Items Out
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts index b4403f5188..1cc7dcbd6e 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts @@ -17,6 +17,7 @@ import {EditToolbarComponent} from './edit-toolbar.component'; 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: [ @@ -27,6 +28,7 @@ import {BarcodesModule} from '@eg/staff/share/barcodes/barcodes.module'; EditComponent, EditToolbarComponent, BcSearchComponent, + ItemsComponent, PrecatCheckoutDialogComponent ], imports: [ diff --git a/Open-ILS/src/eg2/src/app/staff/share/barcodes/barcode-select.component.ts b/Open-ILS/src/eg2/src/app/staff/share/barcodes/barcode-select.component.ts index 4191c1b182..6f2f847904 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/barcodes/barcode-select.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/barcodes/barcode-select.component.ts @@ -56,8 +56,8 @@ export class BarcodeSelectComponent extends DialogComponent implements OnInit { } 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]; @@ -93,9 +93,9 @@ export class BarcodeSelectComponent extends DialogComponent implements OnInit { 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); } }); diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts index 907a85d42a..02d0b9a286 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts @@ -2,15 +2,18 @@ import {NgModule} from '@angular/core'; 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 diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts index 2acffcb874..7bede2fb4b 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts @@ -31,7 +31,7 @@ export interface CheckoutParams { export interface CheckoutResult { index: number; evt: EgEvent; - params: CheckoutParams, + params: CheckoutParams; success: boolean; copy?: IdlObject; circ?: IdlObject; diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.html b/Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.html new file mode 100644 index 0000000000..b4c1f5e6dd --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.html @@ -0,0 +1,40 @@ + + + + + + + {{r.title}} + + {{r.title}} + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.ts new file mode 100644 index 0000000000..7213294088 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/grid.component.ts @@ -0,0 +1,181 @@ +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 { + + // 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(); + } + } + ); + } +} +