From: Bill Erickson Date: Thu, 22 Jul 2021 21:21:16 +0000 (-0400) Subject: LP1936233 Item status list view X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=1c32d0bae0674d40e773ce98e1501ddfa97e2a1d;p=working%2FEvergreen.git LP1936233 Item status list view Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.html b/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.html index 0b184e5fb7..df57ac8d0a 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.html +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.html @@ -26,7 +26,7 @@ text="There is no corresponding purchase order for this item."> -
+
@@ -43,11 +43,16 @@ helpText="Single barcode or list of barcodes separated with commas."> + -- OR -- + + +
@@ -194,8 +194,128 @@ - + [useLocalSort]="true" [sortable]="true" [showDeclaredFieldsOnly]="true" + [disablePaging]="true"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.ts index 7af2f614d9..3dd047a1a5 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/cat/item/status.component.ts @@ -1,4 +1,5 @@ import {Component, Input, OnInit, AfterViewInit, ViewChild} from '@angular/core'; +import {Location} from '@angular/common'; import {Router, ActivatedRoute, ParamMap} from '@angular/router'; import {from, of, empty} from 'rxjs'; import {concatMap, tap} from 'rxjs/operators'; @@ -56,6 +57,9 @@ import {ItemStatusService} from './item.service'; }) export class ItemStatusComponent implements OnInit, AfterViewInit { + // Use for grid row indexes, since the grid may include multiple + // copies of a given item. + static rowIndex = 0; currentItemId: number; itemBarcode: string; @@ -102,6 +106,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { constructor( private router: Router, private route: ActivatedRoute, + private ngLocation: Location, private net: NetService, private idl: IdlService, private printer: PrintService, @@ -149,29 +154,27 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { row.call_number().label() + ' ' + row.call_number().suffix().label(); } - } - - this.worklog.loadSettings().then(_ => this.load(true)); - } - - load(first?: boolean) { + }; - this.cat.fetchCcvms() + this.worklog.loadSettings() + .then(_ => this.cat.fetchCcvms()) .then(_ => this.cat.fetchCmfs()) .then(_ => { + if (this.currentItemId) { - return this.getItemById(this.currentItemId); + + return this.getItemById(this.currentItemId) + .then(item => this.item = item); + } else if (this.preloadCopyIds) { + return from(this.preloadCopyIds).pipe(concatMap(id => { return of(this.getItemById(id)); - })).toPromise().then(_ => this.preloadCopyIds = null); + })).toPromise().then(__ => this.preloadCopyIds = null); } }) .then(_ => { - // Avoid multiple subscriptions - if (!first) { return; } - // Avoid watching for changes until after ngOnInit is complete // so we don't grab the same copy twice. this.route.paramMap.subscribe((params: ParamMap) => { @@ -186,6 +189,29 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { }); } + // Refresh items that were acted on by a grid action and splice them. + refreshSelectCopies(copies: IdlObject[]): Promise { + return from(copies).pipe(concatMap(copy => { + const promise = this.getItemById(copy.id(), true) + .then(updatedCopy => { + if (this.item && updatedCopy.id() === this.item.id()) { + this.item = updatedCopy; + } + this.itemService.scannedItems.forEach((item, idx) => { + if (item.id() === updatedCopy.id()) { + this.itemService.scannedItems.splice(idx, 1, updatedCopy); + } + }); + }); + + return from(promise); + })).toPromise().then(_ => { + if (this.grid) { + this.grid.reload(); + } + }); + } + ngAfterViewInit() { this.selectInput(); } @@ -194,6 +220,30 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { this.router.navigate([`/staff/cat/item/${this.currentItemId}/${evt.nextId}`]); } + fileSelected($event) { + const file: File = $event.target.files[0]; + const reader = new FileReader(); + + reader.onload = e => { + const list = e.target.result as string; + if (!list) { return; } + + const barcodes = []; + list.split(/\r?\n/).forEach(line => { + line = line.replace(/[\s,]+/g, ''); + if (line) { + barcodes.push(line); + } + }); + + if (barcodes.length > 0) { + this.getItemsFromBarcodes(barcodes); + } + }; + + reader.readAsText(file); + } + getItemFromBarcodeInput(): Promise { this.currentItemId = null; this.item = null; @@ -203,41 +253,51 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { // The barcode may be a comma-separated list of values. const barcodes = []; this.itemBarcode.split(/,/).forEach(bc => { - bc = bc.replace(/[\s,]+/g,''); + bc = bc.replace(/[\s,]+/g, ''); if (bc) { barcodes.push(bc); } }); + return this.getItemsFromBarcodes(barcodes); + } + + getItemsFromBarcodes(barcodes: string[]): Promise { let index = 0; + return from(barcodes).pipe(concatMap(bc => { - return of( - this.getOneItemFromBarcode(bc) - .then(_ => { - if (++index < barcodes.length) { return; } - if (this.tab === 'list') { return; } - - // When entering multiple items via input or file - // on a non-list page, show the detail view of the - // last item loaded. - if (this.itemService.scannedItems.length > 0) { - const id = this.itemService.scannedItems[0].id(); - this.router.navigate([`/staff/cat/item/${id}/${this.tab}`]); - } - }) - ); + + const promise = this.getOneItemFromBarcode(bc) + .then(_ => { + if (++index < barcodes.length) { return; } + if (this.tab === 'list') { return; } + + // When entering multiple items via input or file + // on a non-list page, show the detail view of the + // last item loaded. + if (this.itemService.scannedItems.length > 0) { + this.item = this.itemService.scannedItems[0]; + const id = this.item.id(); + this.router.navigate([`/staff/cat/item/${id}/${this.tab}`]); + } + }); + + return from(promise); + })).toPromise(); } getOneItemFromBarcode(barcode: string): Promise { return this.barcodeSelect.getBarcode('asset', barcode) .then(res => { - if (!res.id) { + if (!res) { + // Dialog was canceled, nothing to do + } else if (!res.id) { this.noSuchItem = true; } else { this.itemBarcode = null; if (this.tab === 'list') { this.selectInput(); - return this.getItemById(res.id); } + return this.getItemById(res.id); } }); } @@ -250,9 +310,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { }); } - getItemById(id: number): Promise { - - // TODO fetch open circ and store it on the copy + getItemById(id: number, fetchOnly?: boolean): Promise { const flesh = { flesh : 4, @@ -275,19 +333,21 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { return this.pcrud.retrieve('acp', id, flesh) .toPromise().then(item => { - this.item = item; - this.mungeIsbns(); + this.mungeIsbns(item); this.selectInput(); + item._index = ItemStatusComponent.rowIndex++; - if (this.tab === 'list') { - this.itemService.scannedItems.unshift(item); - } else { - // Only add the copy to the scanned items list on a non-list - // page when the item does not already exist in the list. - const existing = this.itemService.scannedItems - .filter(c => c.id() === item.id())[0]; - if (!existing) { + if (!fetchOnly) { + if (this.tab === 'list') { this.itemService.scannedItems.unshift(item); + } else { + // Only add the copy to the scanned items list on a non-list + // page when the item does not already exist in the list. + const existing = this.itemService.scannedItems + .filter(c => c.id() === item.id())[0]; + if (!existing) { + this.itemService.scannedItems.unshift(item); + } } } @@ -295,6 +355,8 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { if (this.grid) { this.grid.reload(); } + + return item; }); }); } @@ -310,8 +372,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { } // A bit of cleanup to make the ISBN's look friendlier - mungeIsbns() { - const item = this.item; + mungeIsbns(item: IdlObject) { const isbn = item.call_number().record().simple_record().isbn(); if (isbn) { const matches = isbn.match(/"(.*?)"/g); @@ -364,8 +425,18 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { requestItems(copies: IdlObject[]) { if (copies.length === 0) { return; } + const params = {target: copies.map(c => c.id()), holdFor: 'staff'}; - this.router.navigate(['/staff/catalog/hold/C'], {queryParams: params}); + + const url = this.ngLocation.prepareExternalUrl( + this.router.serializeUrl( + this.router.createUrlTree( + ['/staff/catalog/hold/C'], {queryParams: params} + ) + ) + ); + + window.open(url); } openConjoinedDialog(copies: IdlObject[]) { @@ -397,7 +468,10 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { } this.deleteHolding.callNums = Object.values(callNumHash); - this.deleteHolding.open({size: 'sm'}).subscribe(modified => this.load()); + this.deleteHolding.open({size: 'sm'}) + .subscribe(modified => { + if (modified) { this.refreshSelectCopies(copies); } + }); } checkinItems(copies: IdlObject[]) { @@ -419,7 +493,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { }, () => { dialog.close(); - this.load(); + this.refreshSelectCopies(copies); } ); } @@ -443,7 +517,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { }, () => { dialog.close(); - this.load(); + this.refreshSelectCopies(copies); } ); } @@ -453,6 +527,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { // Copies in transit are not always accompanied by their transit. const transitIds = []; + let modified = false; from(copies).pipe(concatMap(c => { return from( this.circ.findCopyTransitById(c.id()) @@ -472,7 +547,15 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { return empty(); } - })).subscribe(); + })).subscribe( + changes => { + if (changes) { modified = true; } + }, + null, + () => { + if (modified) { this.refreshSelectCopies(copies); } + } + ); } updateInventory(copies: IdlObject[]) { @@ -482,7 +565,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { 'open-ils.circ', 'open-ils.circ.circulation.update_latest_inventory', this.auth.token(), {copy_list: copies.map(c => c.id())} - ).toPromise().then(_ => this.load()); + ).toPromise().then(_ => this.refreshSelectCopies(copies)); } printLabels(copies: IdlObject[]) { @@ -524,7 +607,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { return this.markDamagedDialog.open({size: 'lg'}) .pipe(tap(ok => { if (ok) { modified = true; } })); - })).toPromise().then(_ => this.load()); + })).toPromise().then(_ => this.refreshSelectCopies(copies)); } @@ -539,7 +622,9 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { copyId => modified = true, null, () => { - if (modified) { this.load(); } + if (modified) { + this.refreshSelectCopies(copies); + } } ); } @@ -556,7 +641,9 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { copyId => modified = true, null, () => { - if (modified) { this.load(); } + if (modified) { + this.refreshSelectCopies(copies); + } } ); } @@ -586,6 +673,25 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { this.holdings.spawnAddHoldingsUi(null, callNumIds, null, copyIds); } + showInCatalog(copies: IdlObject[]) { + if (copies.length === 0) { return; } + + const recId = copies[0].call_number().record().id(); + const url = this.ngLocation.prepareExternalUrl( + `/staff/catalog/record/${recId}`); + + window.open(url); + } + + showRecordHolds(copies: IdlObject[]) { + if (copies.length === 0) { return; } + + const recId = copies[0].call_number().record().id(); + const url = this.ngLocation.prepareExternalUrl( + `/staff/catalog/record/${recId}/holds`); + + window.open(url); + } // Only the first item is used as the basis for new // call numbers. @@ -605,7 +711,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { this.copyAlertsDialog.open({size: 'lg'}).subscribe( modified => { if (modified) { - this.load(); + this.refreshSelectCopies(copies); } } ); @@ -617,7 +723,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { this.replaceBarcode.open({}).subscribe( modified => { if (modified) { - this.load(); + this.refreshSelectCopies(copies); } } ); @@ -638,7 +744,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { copies = this.idl.clone(copies); // avoid tweaking active data this.transferItems.autoTransferItems(copies, recId, orgId) - .then(success => success ? this.load() : null); + .then(success => success ? this.refreshSelectCopies(copies) : null); } @@ -653,7 +759,7 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { } this.transferItems.transferItems(copies.map(c => c.id()), cnId) - .then(success => success ? this.load() : null); + .then(success => success ? this.refreshSelectCopies(copies) : null); } showDetails(copy?: IdlObject) { @@ -662,23 +768,39 @@ export class ItemStatusComponent implements OnInit, AfterViewInit { if (copy) { // Row doubleclick copyId = copy.id(); - } else if (this.grid) { + + } else if (this.itemService.scannedItems.length > 0) { // Row select + clicking the Show Details button - copyId = this.grid.context.rowSelector.selected()[0]; - } - // No item selected. Use the first one in the list if we have one. - if (!copyId && this.itemService.scannedItems.length > 0) { - copyId = this.itemService.scannedItems[0].id(); + // Grid row selector does not maintain order, so use the first + // grid row that is selected. + this.itemService.scannedItems.some(item => { + if (this.grid.context.rowIsSelected(item)) { + copyId = item.id(); + return true; + } + }); + + // No rows selected, use the first copy row. + if (!copyId) { + copyId = this.itemService.scannedItems[0].id(); + } } - this.router.navigate([`/staff/cat/item/${copyId}/summary`]); + if (copyId) { + this.router.navigate([`/staff/cat/item/${copyId}/summary`]); + } } showList() { this.currentItemId = null; this.router.navigate(['/staff/cat/item/list']); } + + clearList() { + this.itemService.scannedItems = []; + this.grid.reload(); + } }