text="There is no corresponding purchase order for this item.">
</eg-string>
-<div class="row">
+<div class="row mb-3">
<div class="col-lg-12 form-inline d-flex">
<div class="input-group">
<div class="input-group-prepend">
helpText="Single barcode or list of barcodes separated with commas.">
</eg-help-popover>
+ <span class="ml-2 mr-2 font-weight-bold" i18n>-- OR --</span>
+
<eg-help-popover placement="top" i18n-helpText
helpText="File Format: One barcode per line.
All whitespace and commas will be removed before processing.">
</eg-help-popover>
+ <input #fileSelector (change)="fileSelected($event)"
+ id="upload-file" required class="ml-2" type="file"/>
+
<div class="flex-1"></div>
<button *ngIf="tab == 'list'"
List View
</button>
-
<!-- ACTIONS MENU -->
<eg-grouped-menu i18n-label label="Actions" *ngIf="item && tab != 'list'">
<!-- Show -->
<eg-grouped-menu-entry i18n-label label="Show Record Holds"
- i18n-group group="Show" [newTab]="true"
- routerLink="/staff/catalog/record/{{item.call_number().record().id()}}/holds">
+ i18n-group group="Show" (entryClicked)="showRecordHolds([item])">
</eg-grouped-menu-entry>
<eg-grouped-menu-entry i18n-label label="Show In Catalog"
- i18n-group group="Show" [newTab]="true"
- routerLink="/staff/catalog/record/{{item.call_number().record().id()}}">
+ i18n-group group="Show" (entryClicked)="showInCatalog([item])">
</eg-grouped-menu-entry>
<eg-grouped-menu-entry i18n-label label="Show Originating Acquisition"
i18n-group group="Show" (entryClicked)="showAcq([item])">
</eg-grouped-menu>
- <!-- grouped menu todo -->
-
</div>
</div>
<eg-grid *ngIf="tab == 'list'" #grid [dataSource]="dataSource" idlClass="acp"
(onRowActivate)="showDetails($event)" [cellTextGenerator]="cellTextGenerator"
- [useLocalSort]="true" [sortable]="true" [showDeclaredFieldsOnly]="true">
-
+ [useLocalSort]="true" [sortable]="true" [showDeclaredFieldsOnly]="true"
+ [disablePaging]="true">
+
+ <eg-grid-toolbar-button i18n-label label="Clear List" (onClick)="clearList()">
+ </eg-grid-toolbar-button>
+
+ <!-- Un-grouped -->
+ <eg-grid-toolbar-action i18n-label label="Request Items"
+ (onClick)="requestItems($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label
+ label="Link as Conjoined to Marked Bib Record"
+ (onClick)="openConjoinedDialog($event)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action i18n-label label="Update Inventory"
+ (onClick)="updateInventory($event)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action i18n-label label="Print Labels"
+ (onClick)="printLabels($event)">
+ </eg-grid-toolbar-action>
+
+ <!-- Add -->
+ <eg-grid-toolbar-action i18n-label label="Add Item To Bucket"
+ i18n-group group="Add" (onClick)="addItemToBucket($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Add Record To Bucket"
+ i18n-group group="Add" (onClick)="addRecordToBucket($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action
+ i18n-group group="Add" i18n-label label="Add Items"
+ (onClick)="addItems($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action
+ i18n-group group="Add" i18n-label label="Add Call Numbers and Items"
+ (onClick)="addVols($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action
+ i18n-group group="Add" i18n-label label="Add Item Alerts"
+ (onClick)="itemAlerts($event, 'create')">
+ </eg-grid-toolbar-action>
+
+ <!-- Edit -->
+ <eg-grid-toolbar-action i18n-label label="Delete Items"
+ i18n-group group="Edit" (onClick)="deleteItems($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Edit Call Numbers"
+ i18n-group group="Edit" (onClick)="editVols($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Edit Items"
+ i18n-group group="Edit" (onClick)="editItems($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Edit Call Numbers and Items"
+ i18n-group group="Edit" (onClick)="editVolsAndItems($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Replace Barcodes"
+ i18n-group group="Edit" (onClick)="replaceBarcodes($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action
+ i18n-group group="Edit" i18n-label label="Manage Item Alerts"
+ (onClick)="itemAlerts($event, 'manage')">
+ </eg-grid-toolbar-action>
+
+ <!-- Mark -->
+ <eg-grid-toolbar-action i18n-label label="Mark Item as Damaged"
+ i18n-group group="Mark" (onClick)="markDamaged($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Mark Item as Discard/Weed"
+ i18n-group group="Mark" (onClick)="discardWeed($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Mark Item as Missing"
+ i18n-group group="Mark" (onClick)="markMissing($event)">
+ </eg-grid-toolbar-action>
+
+ <!-- Show -->
+ <eg-grid-toolbar-action i18n-label label="Show Record Holds"
+ i18n-group group="Show" (onClick)="showRecordHolds($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Show In Catalog"
+ i18n-group group="Show" (onClick)="showInCatalog($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Show Originating Acquisition"
+ i18n-group group="Show" (onClick)="showAcq($event)">
+ </eg-grid-toolbar-action>
+
+ <!-- Booking -->
+ <eg-grid-toolbar-action i18n-label label="Make Items Bookable"
+ i18n-group group="Booking" (onClick)="makeItemsBookable($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Book Item Now"
+ i18n-group group="Booking" (onClick)="bookItems($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Manage Reservations"
+ i18n-group group="Booking" (onClick)="manageReservations($event)">
+ </eg-grid-toolbar-action>
+
+ <!-- Circulation -->
+ <eg-grid-toolbar-action i18n-label label="Check In Items"
+ i18n-group group="Circulation" (onClick)="checkinItems($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Renew Items"
+ i18n-group group="Circulation" (onClick)="renewItems($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action i18n-label label="Cancel Transit"
+ i18n-group group="Circulation" (onClick)="cancelTransits($event)">
+ </eg-grid-toolbar-action>
+
+ <!-- Transfer -->
+ <eg-grid-toolbar-action i18n-label
+ label="Transfer Items to Previously Marked Library"
+ i18n-group group="Transfer" (onClick)="transferItemsToLib($event)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action i18n-label
+ label="Transfer Items to Previously Marked Call Number"
+ i18n-group group="Transfer" (onClick)="transferItemsToCn($event)">
+ </eg-grid-toolbar-action>
+
+ <!-- columns -->
+
+ <eg-grid-column path="_index" [index]="true" [hidden]="true"></eg-grid-column>
<eg-grid-column path="id" [hidden]="true"></eg-grid-column>
<eg-grid-column path="alert_message"></eg-grid-column>
<eg-grid-column path="barcode"></eg-grid-column>
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';
})
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;
constructor(
private router: Router,
private route: ActivatedRoute,
+ private ngLocation: Location,
private net: NetService,
private idl: IdlService,
private printer: PrintService,
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) => {
});
}
+ // Refresh items that were acted on by a grid action and splice them.
+ refreshSelectCopies(copies: IdlObject[]): Promise<any> {
+ 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();
}
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<any> {
this.currentItemId = null;
this.item = null;
// 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<any> {
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<any> {
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);
}
});
}
});
}
- getItemById(id: number): Promise<any> {
-
- // TODO fetch open circ and store it on the copy
+ getItemById(id: number, fetchOnly?: boolean): Promise<any> {
const flesh = {
flesh : 4,
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);
+ }
}
}
if (this.grid) {
this.grid.reload();
}
+
+ return item;
});
});
}
}
// 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);
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[]) {
}
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[]) {
},
() => {
dialog.close();
- this.load();
+ this.refreshSelectCopies(copies);
}
);
}
},
() => {
dialog.close();
- this.load();
+ this.refreshSelectCopies(copies);
}
);
}
// 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())
return empty();
}
- })).subscribe();
+ })).subscribe(
+ changes => {
+ if (changes) { modified = true; }
+ },
+ null,
+ () => {
+ if (modified) { this.refreshSelectCopies(copies); }
+ }
+ );
}
updateInventory(copies: IdlObject[]) {
'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[]) {
return this.markDamagedDialog.open({size: 'lg'})
.pipe(tap(ok => { if (ok) { modified = true; } }));
- })).toPromise().then(_ => this.load());
+ })).toPromise().then(_ => this.refreshSelectCopies(copies));
}
copyId => modified = true,
null,
() => {
- if (modified) { this.load(); }
+ if (modified) {
+ this.refreshSelectCopies(copies);
+ }
}
);
}
copyId => modified = true,
null,
() => {
- if (modified) { this.load(); }
+ if (modified) {
+ this.refreshSelectCopies(copies);
+ }
}
);
}
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.
this.copyAlertsDialog.open({size: 'lg'}).subscribe(
modified => {
if (modified) {
- this.load();
+ this.refreshSelectCopies(copies);
}
}
);
this.replaceBarcode.open({}).subscribe(
modified => {
if (modified) {
- this.load();
+ this.refreshSelectCopies(copies);
}
}
);
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);
}
}
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) {
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();
+ }
}