current: null
},
holdingTransfer: {
- key: 'eg.cat.marked_holding_transfer_record',
- current: null
+ key: 'eg.cat.transfer_target_record',
+ current: null,
+ clear: [ // Clear these values on mark.
+ 'eg.cat.transfer_target_lib',
+ 'eg.cat.transfer_target_vol'
+ ]
}
};
const target = this.targets[name];
target.current = this.recId;
this.store.setLocalItem(target.key, this.recId);
+
+ if (target.clear) {
+ // Some marks require clearing other marks.
+ target.clear.forEach(key => this.store.removeLocalItem(key));
+ }
+
this.strings.interpolate('catalog.record.toast.' + name)
.then(txt => this.toast.success(txt));
}
<eg-bucket-dialog #bucketDialog></eg-bucket-dialog>
<eg-conjoined-items-dialog #conjoinedDialog></eg-conjoined-items-dialog>
<eg-make-bookable-dialog #makeBookableDialog></eg-make-bookable-dialog>
+<eg-transfer-items #transferItems></eg-transfer-items>
+<eg-transfer-holdings #transferHoldings></eg-transfer-holdings>
+<eg-alert-dialog #transferAlert
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="No Target Selected"
+ dialogBody="Please select a suitable transfer target"></eg-alert-dialog>
<!-- holdings grid -->
<div class='eg-copies w-100 mt-3'>
i18n-label label="Mark Library/Call Number as Transfer Destination"
(onClick)="markLibCnForTransfer($event)">
</eg-grid-toolbar-action>
-
+
+ <eg-grid-toolbar-action
+ i18n-group group="Transfer"
+ i18n-label label="Transfer Items to Marked Destination"
+ (onClick)="transferSelectedItems($event)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Transfer"
+ i18n-label label="Transfer Holdings to Marked Destination"
+ (onClick)="transferSelectedHoldings($event)">
+ </eg-grid-toolbar-action>
<!-- fields -->
<!-- NOTE column names were added to match the names from the AngJS grid
} from '@eg/staff/share/holdings/conjoined-items-dialog.component';
import {MakeBookableDialogComponent
} from '@eg/staff/share/booking/make-bookable-dialog.component';
+import {TransferItemsComponent
+ } from '@eg/staff/share/holdings/transfer-items.component';
+import {TransferHoldingsComponent
+ } from '@eg/staff/share/holdings/transfer-holdings.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+
// The holdings grid models a single HoldingsTree, composed of HoldingsTreeNodes
// flattened on-demand into a list of HoldingEntry objects.
private conjoinedDialog: ConjoinedItemsDialogComponent;
@ViewChild('makeBookableDialog', { static: true })
private makeBookableDialog: MakeBookableDialogComponent;
+ @ViewChild('transferItems', {static: false})
+ private transferItems: TransferItemsComponent;
+ @ViewChild('transferHoldings', {static: false})
+ private transferHoldings: TransferHoldingsComponent;
+ @ViewChild('transferAlert', {static: false})
+ private transferAlert: AlertDialogComponent;
holdingsTree: HoldingsTree;
// Which copies in the grid are selected.
selectedCopyIds(rows: HoldingsEntry[], skipStatus?: number): number[] {
+ return this.selectedCopies(rows, skipStatus).map(c => Number(c.id()));
+ }
+
+ selectedCopies(rows: HoldingsEntry[], skipStatus?: number): IdlObject[] {
let copyRows = rows.filter(r => Boolean(r.copy)).map(r => r.copy);
if (skipStatus) {
copyRows = copyRows.filter(
c => Number(c.status().id()) !== Number(skipStatus));
}
- return copyRows.map(c => Number(c.id()));
+ return copyRows;
}
selectedCallNumIds(rows: HoldingsEntry[]): number[] {
+ return this.selectedCallNums(rows).map(cn => cn.id());
+ }
+
+ selectedCallNums(rows: HoldingsEntry[]): IdlObject[] {
return rows
.filter(r => r.treeNode.nodeType === 'callNum')
- .map(r => Number(r.callNum.id()));
+ .map(r => r.callNum);
}
+
async showMarkDamagedDialog(rows: HoldingsEntry[]) {
const copyIds = this.selectedCopyIds(rows, 14 /* ignore damaged */);
// Action may only apply to a single org or call number row.
const node = rows[0].treeNode;
- if (node.nodeType === 'copy') {
- return;
- }
+ if (node.nodeType === 'copy') { return; }
let orgId: number;
'eg.cat.transfer_target_vol', node.target.id());
}
- this.localStore.setLocalItem('eg.cat.transfer_target_record', this.recordId);
+ // Track lib and record to support transfering items from
+ // a different bib record to this record at the selected
+ // owning lib.
this.localStore.setLocalItem('eg.cat.transfer_target_lib', orgId);
+ this.localStore.setLocalItem('eg.cat.transfer_target_record', this.recordId);
}
openAngJsWindow(path: string) {
);
}
}
+
+ transferSelectedItems(rows: HoldingsEntry[]) {
+ if (rows.length === 0) { return; }
+
+ const cnId =
+ this.localStore.getLocalItem('eg.cat.transfer_target_vol');
+
+ const orgId =
+ this.localStore.getLocalItem('eg.cat.transfer_target_lib');
+
+ const recId =
+ this.localStore.getLocalItem('eg.cat.transfer_target_record');
+
+ let promise;
+
+ if (cnId) { // Direct call number transfer
+
+ const itemIds = this.selectedCopyIds(rows);
+ promise = this.transferItems.transferItems(itemIds, cnId);
+
+ } else if (orgId && recId) { // "Auto" transfer
+
+ // Clone the items to be modified to avoid any unexpected
+ // modifications and fesh the call numbers.
+ const items = this.idl.clone(this.selectedCopies(rows));
+ items.forEach(i => i.call_number(
+ this.treeNodeCache.callNum[i.call_number()].target));
+
+ console.log(items);
+ promise = this.transferItems.autoTransferItems(items, recId, orgId);
+
+ } else {
+ promise = this.transferAlert.open().toPromise();
+ }
+
+ promise.then(success => success ? this.hardRefresh() : null);
+ }
+
+ transferSelectedHoldings(rows: HoldingsEntry[]) {
+ const callNums = this.selectedCallNums(rows);
+ if (callNums.length === 0) { return; }
+
+ const orgId =
+ this.localStore.getLocalItem('eg.cat.transfer_target_lib');
+
+ let recId =
+ this.localStore.getLocalItem('eg.cat.transfer_target_record');
+
+ if (orgId) {
+ // When transferring holdings (call numbers) between org units,
+ // limit transfers to within the current record.
+ recId = this.recordId;
+
+ } else if (!recId) {
+ // No destinations applied.
+ return this.transferAlert.open();
+ }
+
+ this.transferHoldings.targetRecId = recId;
+ this.transferHoldings.targetOrgId = orgId;
+ this.transferHoldings.callNums = callNums;
+
+ this.transferHoldings.transferHoldings()
+ .then(success => success ? this.hardRefresh() : null);
+ }
}
+
import {ReplaceBarcodeDialogComponent} from './replace-barcode-dialog.component';
import {DeleteHoldingDialogComponent} from './delete-volcopy-dialog.component';
import {ConjoinedItemsDialogComponent} from './conjoined-items-dialog.component';
+import {TransferItemsComponent} from './transfer-items.component';
+import {TransferHoldingsComponent} from './transfer-holdings.component';
@NgModule({
declarations: [
CopyAlertsDialogComponent,
ReplaceBarcodeDialogComponent,
DeleteHoldingDialogComponent,
- ConjoinedItemsDialogComponent
+ ConjoinedItemsDialogComponent,
+ TransferItemsComponent,
+ TransferHoldingsComponent
],
imports: [
StaffCommonModule
CopyAlertsDialogComponent,
ReplaceBarcodeDialogComponent,
DeleteHoldingDialogComponent,
- ConjoinedItemsDialogComponent
+ ConjoinedItemsDialogComponent,
+ TransferItemsComponent,
+ TransferHoldingsComponent
],
providers: [
HoldingsService
--- /dev/null
+
+<eg-string #successMsg
+ text="Successfully Transferred Item(s)" i18n-text></eg-string>
+
+<eg-string #errorMsg
+ text="Failed To Transfer Item(s)" i18n-text></eg-string>
+
+<eg-string #noTargetMsg
+ text="No transfer target selected" i18n-text></eg-string>
+
+<eg-alert-dialog #alertDialog
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="One or more items could not be transferred"
+ dialogBody="Reason(s) include: {{eventDesc}}">
+</eg-alert-dialog>
+
+<eg-progress-dialog #progressDialog></eg-progress-dialog>
--- /dev/null
+import {Component, OnInit, Input, ViewChild, Renderer2} from '@angular/core';
+import {Observable} from 'rxjs';
+import {switchMap, map, tap} from 'rxjs/operators';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+
+/* Transfer holdings (AKA asset.call_number) to a target bib record. */
+
+@Component({
+ selector: 'eg-transfer-holdings',
+ templateUrl: 'transfer-holdings.component.html'
+})
+
+export class TransferHoldingsComponent implements OnInit {
+
+ // Array of 'acn' objects.
+ // Assumes all acn's are children of the same bib record.
+ @Input() callNums: IdlObject[];
+
+ // Required field.
+ // All call numbers will be transferred to this record ID.
+ @Input() targetRecId: number;
+
+ // Optional. If set, all call numbers will transfer to this org
+ // unit (owning lib) in addition to transfering to the select bib
+ // record.
+ @Input() targetOrgId: number;
+
+ @ViewChild('successMsg', {static: false})
+ private successMsg: StringComponent;
+
+ @ViewChild('noTargetMsg', {static: false})
+ private noTargetMsg: StringComponent;
+
+ @ViewChild('alertDialog', {static: false})
+ private alertDialog: AlertDialogComponent;
+
+ @ViewChild('progressDialog', {static: false})
+ private progressDialog: ProgressDialogComponent;
+
+ eventDesc: string;
+
+ constructor(
+ private toast: ToastService,
+ private net: NetService,
+ private auth: AuthService,
+ private evt: EventService) {}
+
+ ngOnInit() {}
+
+ // Resolves with true if transfer completed, false otherwise.
+ // Assumes all volumes are transferred to the same bib record.
+ transferHoldings(): Promise<Boolean> {
+ if (!this.callNums || this.callNums.length === 0) {
+ return Promise.resolve(false);
+ }
+
+ if (!this.targetRecId) {
+ this.toast.warning(this.noTargetMsg.text);
+ return Promise.resolve(false);
+ }
+
+ this.eventDesc = '';
+
+ // Group the transfers by owning library.
+ const transferVols: {[orgId: number]: number[]} = {};
+
+ if (this.targetOrgId) {
+
+ // Transfering all call numbers to the same bib record
+ // and owning library.
+ transferVols[+this.targetOrgId] = this.callNums.map(cn => cn.id());
+
+ } else {
+
+ // Transfering all call numbers to the same bib record
+ // while retaining existing owning library.
+ this.callNums.forEach(cn => {
+ const orgId = Number(cn.owning_lib());
+ if (!transferVols[orgId]) { transferVols[orgId] = []; }
+ transferVols[orgId].push(cn.id());
+ });
+ }
+
+ this.progressDialog.update({
+ value: 0,
+ max: Object.keys(transferVols).length
+ });
+ this.progressDialog.open();
+
+ return this.performTransfers(transferVols)
+ .then(res => {
+ this.progressDialog.close();
+ return res;
+ });
+ }
+
+ performTransfers(transferVols: any): Promise<Boolean> {
+ const orgId = Object.keys(transferVols)[0];
+ const volIds = transferVols[orgId];
+
+ // Avoid re-processing
+ delete transferVols[orgId];
+
+ // Note the AngJS client also assumes .override.
+ const method = 'open-ils.cat.asset.volume.batch.transfer.override';
+
+ return this.net.request('open-ils.cat', method, this.auth.token(), {
+ docid: this.targetRecId,
+ lib: orgId,
+ volumes: volIds
+ }).toPromise().then(resp => {
+ const evt = this.evt.parse(resp);
+
+ if (evt || Number(resp) !== 1) {
+ console.warn(resp);
+ this.eventDesc = evt ? evt.desc : '';
+
+ // Failure -- stop short there to avoid alert storm.
+ return this.alertDialog.open().toPromise()
+ .then(_ => { this.eventDesc = ''; return false; });
+ }
+
+ this.progressDialog.increment();
+
+ if (Object.keys(transferVols).length > 0) {
+ return this.performTransfers(transferVols);
+ }
+
+ return true; // All done
+ });
+ }
+}
+
+
+
--- /dev/null
+
+<eg-string #successMsg
+ text="Successfully Transferred Item(s)" i18n-text></eg-string>
+
+<eg-string #errorMsg
+ text="Failed To Transfer Item(s)" i18n-text></eg-string>
+
+<eg-string #noTargetMsg
+ text="No transfer target selected" i18n-text></eg-string>
+
+<eg-confirm-dialog #confirmDialog
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="One or more items could not be transferred. Override?"
+ dialogBody="Reason(s) include: {{eventDesc}}">
+</eg-confirm-dialog>
+
+<eg-alert-dialog #alertDialog
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="One or more items could not be transferred"
+ dialogBody="Reason(s) include: {{eventDesc}}">
+</eg-alert-dialog>
+
--- /dev/null
+import {Component, OnInit, Input, ViewChild, Renderer2} from '@angular/core';
+import {Observable} from 'rxjs';
+import {switchMap, map, tap} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {StringComponent} from '@eg/share/string/string.component';
+
+/* Transfer items to a call number. */
+
+@Component({
+ selector: 'eg-transfer-items',
+ templateUrl: 'transfer-items.component.html'
+})
+
+export class TransferItemsComponent implements OnInit {
+
+ @ViewChild('successMsg', {static: false})
+ private successMsg: StringComponent;
+
+ @ViewChild('errorMsg', {static: false})
+ private errorMsg: StringComponent;
+
+ @ViewChild('noTargetMsg', {static: false})
+ private noTargetMsg: StringComponent;
+
+ @ViewChild('confirmDialog', {static: false})
+ private confirmDialog: ConfirmDialogComponent;
+
+ @ViewChild('alertDialog', {static: false})
+ private alertDialog: AlertDialogComponent;
+
+ eventDesc: string;
+
+ constructor(
+ private toast: ToastService,
+ private idl: IdlService,
+ private net: NetService,
+ private auth: AuthService,
+ private evt: EventService) {}
+
+ ngOnInit() {}
+
+ // Transfers a set of items/copies (by ID) to the selected call
+ // number (by ID).
+ // Resolves with true if transfer completed, false otherwise.
+ transferItems(itemIds: number[],
+ cnId: number, override?: boolean): Promise<boolean> {
+
+ this.eventDesc = '';
+
+ let method = 'open-ils.cat.transfer_copies_to_volume';
+ if (override) { method += '.override'; }
+
+ return this.net.request('open-ils.cat',
+ method, this.auth.token(), cnId, itemIds)
+ .toPromise().then(resp => {
+
+ const evt = this.evt.parse(resp);
+
+ if (evt) {
+
+ if (override) {
+ // Override failed, no looping please.
+ this.toast.warning(this.errorMsg.text);
+ return false;
+ }
+
+ this.eventDesc = evt.desc;
+
+ return this.confirmDialog.open().toPromise().then(ok =>
+ ok ? this.transferItems(itemIds, cnId, true) : false);
+
+ } else { // success
+
+ this.toast.success(this.successMsg.text);
+ return true;
+ }
+ });
+ }
+
+ // Transfers a set of items/copies (by object with fleshed call numbers)
+ // to the selected record and org unit ID, creating new call numbers
+ // where needed.
+ // Resolves with true if transfer completed, false otherwise.
+ autoTransferItems(items: IdlObject[], // acp with fleshed call_number's
+ recId: number, orgId: number): Promise<Boolean> {
+
+ this.eventDesc = '';
+
+ const cnTransfers: any = {};
+ const itemTransfers: any = {};
+
+ items.forEach(item => {
+ const cn = item.call_number();
+
+ if (cn.owning_lib() !== orgId || cn.record() !== recId) {
+ cn.owning_lib(orgId);
+ cn.record(recId);
+
+ if (cnTransfers[cn.id()]) {
+ itemTransfers[cn.id()].push(item.id());
+
+ } else {
+ cnTransfers[cn.id()] = cn;
+ itemTransfers[cn.id()] = [item.id()];
+ }
+ }
+ });
+
+ return this.transferCallNumbers(cnTransfers, itemTransfers);
+ }
+
+ transferCallNumbers(cnTransfers, itemTransfers): Promise<boolean> {
+
+ const cnId = Object.keys(cnTransfers)[0];
+ const cn = cnTransfers[cnId];
+ delete cnTransfers[cnId];
+
+ return this.net.request('open-ils.cat',
+ 'open-ils.cat.call_number.find_or_create',
+ this.auth.token(),
+ cn.label(),
+ cn.record(), // may be new
+ cn.owning_lib(), // may be new
+ (typeof cn.prefix() === 'object' ? cn.prefix().id() : cn.prefix()),
+ (typeof cn.suffix() === 'object' ? cn.suffix().id() : cn.suffix()),
+ cn.label_class()
+
+ ).toPromise().then(resp => {
+
+ const evt = this.evt.parse(resp);
+
+ if (evt) {
+ // Problem. Stop processing.
+ this.toast.warning(this.errorMsg.text);
+ this.eventDesc = evt.desc;
+ return this.alertDialog.open().toPromise().then(_ => false);
+ }
+
+ return this.transferItems(itemTransfers[cn.id()], resp.acn_id)
+ .then(ok => {
+
+ if (ok && Object.keys(cnTransfers).length > 0) {
+ // More call numbers to transfer.
+ return this.transferCallNumbers(cnTransfers, itemTransfers);
+ }
+
+ return ok;
+ });
+ });
+ }
+}
+
+
+