1 import {Component, OnInit, Input, ViewChild} from '@angular/core';
2 import {Router} from '@angular/router';
3 import {Observable, Observer, of} from 'rxjs';
4 import {map} from 'rxjs/operators';
5 import {Pager} from '@eg/share/util/pager';
6 import {IdlObject, IdlService} from '@eg/core/idl.service';
7 import {StaffCatalogService} from '../catalog.service';
8 import {OrgService} from '@eg/core/org.service';
9 import {PcrudService} from '@eg/core/pcrud.service';
10 import {AuthService} from '@eg/core/auth.service';
11 import {GridDataSource} from '@eg/share/grid/grid';
12 import {GridComponent} from '@eg/share/grid/grid.component';
13 import {GridToolbarCheckboxComponent
14 } from '@eg/share/grid/grid-toolbar-checkbox.component';
15 import {StoreService} from '@eg/core/store.service';
16 import {ServerStoreService} from '@eg/core/server-store.service';
17 import {MarkDamagedDialogComponent
18 } from '@eg/staff/share/holdings/mark-damaged-dialog.component';
19 import {MarkMissingDialogComponent
20 } from '@eg/staff/share/holdings/mark-missing-dialog.component';
21 import {AnonCacheService} from '@eg/share/util/anon-cache.service';
22 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
23 import {CopyAlertsDialogComponent
24 } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
25 import {ReplaceBarcodeDialogComponent
26 } from '@eg/staff/share/holdings/replace-barcode-dialog.component';
27 import {DeleteVolcopyDialogComponent
28 } from '@eg/staff/share/holdings/delete-volcopy-dialog.component';
29 import {BucketDialogComponent
30 } from '@eg/staff/share/buckets/bucket-dialog.component';
31 import {ConjoinedItemsDialogComponent
32 } from '@eg/staff/share/holdings/conjoined-items-dialog.component';
33 import {MakeBookableDialogComponent
34 } from '@eg/staff/share/booking/make-bookable-dialog.component';
36 // The holdings grid models a single HoldingsTree, composed of HoldingsTreeNodes
37 // flattened on-demand into a list of HoldingEntry objects.
38 class HoldingsTreeNode {
39 children: HoldingsTreeNode[];
40 nodeType: 'org' | 'volume' | 'copy';
42 parentNode: HoldingsTreeNode;
52 root: HoldingsTreeNode;
54 this.root = new HoldingsTreeNode();
60 // org unit shortname, call number label, or copy barcode
61 locationLabel: string;
62 // location label indentation depth
63 locationDepth: number | null;
64 volumeCount: number | null;
65 copyCount: number | null;
66 callNumberLabel: string;
70 treeNode: HoldingsTreeNode;
74 selector: 'eg-holdings-maintenance',
75 templateUrl: 'holdings.component.html',
76 styleUrls: ['holdings.component.css']
78 export class HoldingsMaintenanceComponent implements OnInit {
81 gridDataSource: GridDataSource;
82 gridTemplateContext: any;
83 @ViewChild('holdingsGrid') holdingsGrid: GridComponent;
85 // Manage visibility of various sub-sections
86 @ViewChild('volsCheckbox')
87 private volsCheckbox: GridToolbarCheckboxComponent;
88 @ViewChild('copiesCheckbox')
89 private copiesCheckbox: GridToolbarCheckboxComponent;
90 @ViewChild('emptyVolsCheckbox')
91 private emptyVolsCheckbox: GridToolbarCheckboxComponent;
92 @ViewChild('emptyLibsCheckbox')
93 private emptyLibsCheckbox: GridToolbarCheckboxComponent;
94 @ViewChild('markDamagedDialog')
95 private markDamagedDialog: MarkDamagedDialogComponent;
96 @ViewChild('markMissingDialog')
97 private markMissingDialog: MarkMissingDialogComponent;
98 @ViewChild('copyAlertsDialog')
99 private copyAlertsDialog: CopyAlertsDialogComponent;
100 @ViewChild('replaceBarcode')
101 private replaceBarcode: ReplaceBarcodeDialogComponent;
102 @ViewChild('deleteVolcopy')
103 private deleteVolcopy: DeleteVolcopyDialogComponent;
104 @ViewChild('bucketDialog')
105 private bucketDialog: BucketDialogComponent;
106 @ViewChild('conjoinedDialog')
107 private conjoinedDialog: ConjoinedItemsDialogComponent;
108 @ViewChild('makeBookableDialog')
109 private makeBookableDialog: MakeBookableDialogComponent;
111 holdingsTree: HoldingsTree;
113 // nodeType => id => tree node cache
114 treeNodeCache: {[nodeType: string]: {[id: number]: HoldingsTreeNode}};
116 // When true and a grid reload is called, the holdings data will be
117 // re-fetched from the server.
118 refreshHoldings: boolean;
120 // Used as a row identifier in th grid, since we're mixing object types.
123 // List of copies whose due date we need to retrieve.
124 itemCircsNeeded: IdlObject[];
126 // When true draw the grid based on the stored preferences.
127 // When not true, render based on the current "expanded" state of each node.
128 // Rendering from prefs happens on initial load and when any prefs change.
129 renderFromPrefs: boolean;
131 rowClassCallback: (row: any) => string;
133 private _recId: number;
134 @Input() set recordId(id: number) {
136 // Only force new data collection when recordId()
137 // is invoked after ngInit() has already run.
142 get recordId(): number {
146 contextOrg: IdlObject;
149 private router: Router,
150 private org: OrgService,
151 private idl: IdlService,
152 private pcrud: PcrudService,
153 private auth: AuthService,
154 private staffCat: StaffCatalogService,
155 private store: ServerStoreService,
156 private localStore: StoreService,
157 private holdings: HoldingsService,
158 private anonCache: AnonCacheService
160 // Set some sane defaults before settings are loaded.
161 this.gridDataSource = new GridDataSource();
162 this.refreshHoldings = true;
163 this.renderFromPrefs = true;
165 // TODO: need a separate setting for this?
166 this.contextOrg = this.staffCat.searchContext.searchOrg;
168 this.rowClassCallback = (row: any): string => {
171 return 'holdings-copy-row';
173 return 'holdings-volume-row';
176 // Add a generic org unit class and a depth-specific
177 // class for styling different levels of the org tree.
178 return 'holdings-org-row holdings-org-row-' +
179 row.treeNode.target.ou_type().depth();
183 this.gridTemplateContext = {
184 toggleExpandRow: (row: HoldingsEntry) => {
185 row.treeNode.expanded = !row.treeNode.expanded;
187 if (!row.treeNode.expanded) {
188 // When collapsing a node, all child nodes should be
189 // collapsed as well.
190 const traverse = (node: HoldingsTreeNode) => {
191 node.expanded = false;
192 node.children.forEach(traverse);
194 traverse(row.treeNode);
197 this.holdingsGrid.reload();
200 copyIsHoldable: (copy: IdlObject): boolean => {
201 return copy.holdable() === 't'
202 && copy.location().holdable() === 't'
203 && copy.status().holdable() === 't';
209 this.initDone = true;
211 // These are pre-cached via the catalog resolver.
212 const settings = this.store.getItemBatchCached([
213 'cat.holdings_show_empty_org',
214 'cat.holdings_show_empty',
215 'cat.holdings_show_copies',
216 'cat.holdings_show_vols'
219 // Show volumes by default when no preference is set.
220 let showVols = settings['cat.holdings_show_vols'];
221 if (showVols === null) { showVols = true; }
223 this.volsCheckbox.checked(showVols);
224 this.copiesCheckbox.checked(settings['cat.holdings_show_copies']);
225 this.emptyVolsCheckbox.checked(settings['cat.holdings_show_empty']);
226 this.emptyLibsCheckbox.checked(settings['cat.holdings_show_empty_org']);
228 this.initHoldingsTree();
229 this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
230 return this.fetchHoldings(pager);
234 contextOrgChanged(org: IdlObject) {
235 this.contextOrg = org;
240 this.renderFromPrefs = true;
241 this.refreshHoldings = true;
242 this.initHoldingsTree();
243 this.holdingsGrid.reload();
246 toggleShowCopies(value: boolean) {
247 this.store.setItem('cat.holdings_show_copies', value);
249 // Showing copies implies showing volumes
250 this.volsCheckbox.checked(true);
252 this.renderFromPrefs = true;
253 this.holdingsGrid.reload();
256 toggleShowVolumes(value: boolean) {
257 this.store.setItem('cat.holdings_show_vols', value);
259 // Hiding volumes implies hiding empty vols and copies.
260 this.copiesCheckbox.checked(false);
261 this.emptyVolsCheckbox.checked(false);
263 this.renderFromPrefs = true;
264 this.holdingsGrid.reload();
267 toggleShowEmptyVolumes(value: boolean) {
268 this.store.setItem('cat.holdings_show_empty', value);
270 this.volsCheckbox.checked(true);
272 this.renderFromPrefs = true;
273 this.holdingsGrid.reload();
276 toggleShowEmptyLibs(value: boolean) {
277 this.store.setItem('cat.holdings_show_empty_org', value);
278 this.renderFromPrefs = true;
279 this.holdingsGrid.reload();
282 onRowActivate(row: any) {
284 // Launch copy editor?
286 this.gridTemplateContext.toggleExpandRow(row);
292 const visibleOrgs = this.org.fullPath(this.contextOrg, true);
294 // The initial tree simply matches the org unit tree
295 const traverseOrg = (node: HoldingsTreeNode) => {
296 node.target.children().forEach((org: IdlObject) => {
297 if (visibleOrgs.indexOf(org.id()) === -1) {
298 return; // Org is outside of scope
300 const nodeChild = new HoldingsTreeNode();
301 nodeChild.nodeType = 'org';
302 nodeChild.target = org;
303 nodeChild.parentNode = node;
304 node.children.push(nodeChild);
305 this.treeNodeCache.org[org.id()] = nodeChild;
306 traverseOrg(nodeChild);
310 this.treeNodeCache = {
316 this.holdingsTree = new HoldingsTree();
317 this.holdingsTree.root.nodeType = 'org';
318 this.holdingsTree.root.target = this.org.root();
319 this.treeNodeCache.org[this.org.root().id()] = this.holdingsTree.root;
321 traverseOrg(this.holdingsTree.root);
324 // Org node children are sorted with any child org nodes pushed to the
325 // front, followed by the call number nodes sorted alphabetcially by label.
326 sortOrgNodeChildren(node: HoldingsTreeNode) {
327 node.children = node.children.sort((a, b) => {
328 if (a.nodeType === 'org') {
329 if (b.nodeType === 'org') {
330 return a.target.shortname() < b.target.shortname() ? -1 : 1;
334 } else if (b.nodeType === 'org') {
337 // TODO: should this use label sortkey instead of
338 // the compiled volume label?
339 return a.target._label < b.target._label ? -1 : 1;
344 // Sets call number and copy count sums to nodes that need it.
345 // Applies the initial expansed state of each container node.
346 setTreeCounts(node: HoldingsTreeNode) {
348 if (node.nodeType === 'org') {
350 node.volumeCount = 0;
351 } else if (node.nodeType === 'volume') {
355 let hasChildOrgWithData = false;
356 let hasChildOrgSansData = false;
357 node.children.forEach(child => {
358 this.setTreeCounts(child);
359 if (node.nodeType === 'org') {
360 node.copyCount += child.copyCount;
361 if (child.nodeType === 'volume') {
364 hasChildOrgWithData = child.volumeCount > 0;
365 hasChildOrgSansData = child.volumeCount === 0;
366 node.volumeCount += child.volumeCount;
368 } else if (node.nodeType === 'volume') {
369 node.copyCount = node.children.length;
370 if (this.renderFromPrefs) {
371 node.expanded = this.copiesCheckbox.checked();
376 if (this.renderFromPrefs && node.nodeType === 'org') {
377 if (node.copyCount > 0 && this.volsCheckbox.checked()) {
378 node.expanded = true;
379 } else if (node.volumeCount > 0 && this.emptyVolsCheckbox.checked()) {
380 node.expanded = true;
381 } else if (hasChildOrgWithData) {
382 node.expanded = true;
383 } else if (hasChildOrgSansData && this.emptyLibsCheckbox.checked()) {
384 node.expanded = true;
386 node.expanded = false;
391 // Create HoldingsEntry objects for tree nodes that should be displayed
392 // and relays them to the grid via the observer.
393 propagateTreeEntries(observer: Observer<HoldingsEntry>, node: HoldingsTreeNode) {
394 const entry = new HoldingsEntry();
395 entry.treeNode = node;
396 entry.index = this.gridIndex++;
398 switch (node.nodeType) {
400 if (node.volumeCount === 0
401 && !this.emptyLibsCheckbox.checked()) {
404 entry.locationLabel = node.target.shortname();
405 entry.locationDepth = node.target.ou_type().depth();
406 entry.copyCount = node.copyCount;
407 entry.volumeCount = node.volumeCount;
408 this.sortOrgNodeChildren(node);
412 if (this.renderFromPrefs) {
413 if (!this.volsCheckbox.checked()) {
416 if (node.copyCount === 0
417 && !this.emptyVolsCheckbox.checked()) {
421 entry.locationLabel = node.target._label;
422 entry.locationDepth = node.parentNode.target.ou_type().depth() + 1;
423 entry.callNumberLabel = entry.locationLabel;
424 entry.volume = node.target;
425 entry.copyCount = node.copyCount;
429 entry.locationLabel = node.target.barcode();
430 entry.locationDepth = node.parentNode.parentNode.target.ou_type().depth() + 2;
431 entry.callNumberLabel = node.parentNode.target.label(); // TODO
432 entry.volume = node.parentNode.target;
433 entry.copy = node.target;
434 entry.circ = node.target._circ;
438 // Tell the grid about the node entry
439 observer.next(entry);
442 // Process the child nodes.
443 node.children.forEach(child =>
444 this.propagateTreeEntries(observer, child));
448 // Turns the tree into a list of entries for grid display
449 flattenHoldingsTree(observer: Observer<HoldingsEntry>) {
451 this.setTreeCounts(this.holdingsTree.root);
452 this.propagateTreeEntries(observer, this.holdingsTree.root);
454 this.renderFromPrefs = false;
457 // Grab volumes, copies, and related data.
458 fetchHoldings(pager: Pager): Observable<any> {
459 if (!this.recordId) { return of([]); }
461 return new Observable<any>(observer => {
463 if (!this.refreshHoldings) {
464 this.flattenHoldingsTree(observer);
468 this.itemCircsNeeded = [];
470 this.pcrud.search('acn',
471 { record: this.recordId,
472 owning_lib: this.org.fullPath(this.contextOrg, true),
474 label: {'!=' : '##URI##'}
478 acp: ['status', 'location', 'circ_lib', 'parts',
479 'age_protect', 'copy_alerts', 'latest_inventory'],
480 acn: ['prefix', 'suffix', 'copies'],
481 acli: ['inventory_workstation']
484 {authoritative: true}
486 vol => this.appendVolume(vol),
489 this.refreshHoldings = false;
490 this.fetchCircs().then(
491 ok => this.flattenHoldingsTree(observer)
498 // Retrieve circulation objects for checked out items.
499 fetchCircs(): Promise<any> {
500 const copyIds = this.itemCircsNeeded.map(copy => copy.id());
501 if (copyIds.length === 0) { return Promise.resolve(); }
503 return this.pcrud.search('circ', {
504 target_copy: copyIds,
506 }).pipe(map(circ => {
507 const copy = this.itemCircsNeeded.filter(
508 c => Number(c.id()) === Number(circ.target_copy()))[0];
513 // Compile prefix + label + suffix into field volume._label;
514 setVolumeLabel(volume: IdlObject) {
515 const pfx = volume.prefix() ? volume.prefix().label() : '';
516 const sfx = volume.suffix() ? volume.suffix().label() : '';
517 volume._label = pfx ? pfx + ' ' : '';
518 volume._label += volume.label();
519 volume._label += sfx ? ' ' + sfx : '';
522 // Create the tree node for the volume if it doesn't already exist.
523 // Do the same for its linked copies.
524 appendVolume(volume: IdlObject) {
525 let volNode = this.treeNodeCache.volume[volume.id()];
526 this.setVolumeLabel(volume);
529 const pNode = this.treeNodeCache.org[volume.owning_lib()];
530 if (volNode.parentNode.target.id() !== pNode.target.id()) {
531 // Volume owning library changed. Un-link it from the previous
532 // org unit collection before adding to the new one.
534 volNode.parentNode = pNode;
535 volNode.parentNode.children.push(volNode);
538 volNode = new HoldingsTreeNode();
539 volNode.nodeType = 'volume';
540 volNode.parentNode = this.treeNodeCache.org[volume.owning_lib()];
541 volNode.parentNode.children.push(volNode);
542 this.treeNodeCache.volume[volume.id()] = volNode;
545 volNode.target = volume;
548 .filter((copy: IdlObject) => (copy.deleted() !== 't'))
549 .sort((a: IdlObject, b: IdlObject) => a.barcode() < b.barcode() ? -1 : 1)
550 .forEach((copy: IdlObject) => this.appendCopy(volNode, copy));
553 // Find or create a copy node.
554 appendCopy(volNode: HoldingsTreeNode, copy: IdlObject) {
555 let copyNode = this.treeNodeCache.copy[copy.id()];
558 const oldParent = copyNode.parentNode;
559 if (oldParent.target.id() !== volNode.target.id()) {
560 // TODO: copy changed owning volume. Remove it from
561 // the previous volume before adding to the new volume.
562 copyNode.parentNode = volNode;
563 volNode.children.push(copyNode);
567 copyNode = new HoldingsTreeNode();
568 copyNode.nodeType = 'copy';
569 volNode.children.push(copyNode);
570 copyNode.parentNode = volNode;
571 this.treeNodeCache.copy[copy.id()] = copyNode;
574 copyNode.target = copy;
575 const stat = Number(copy.status().id());
577 if (stat === 1 /* checked out */ || stat === 16 /* long overdue */) {
578 // Avoid looking up circs on items that are not checked out.
579 this.itemCircsNeeded.push(copy);
583 // Which copies in the grid are selected.
584 selectedCopyIds(rows: HoldingsEntry[], skipStatus?: number): number[] {
585 let copyRows = rows.filter(r => Boolean(r.copy)).map(r => r.copy);
587 copyRows = copyRows.filter(
588 c => Number(c.status().id()) !== Number(skipStatus));
590 return copyRows.map(c => Number(c.id()));
593 selectedVolumeIds(rows: HoldingsEntry[]): number[] {
595 .filter(r => r.treeNode.nodeType === 'volume')
596 .map(r => Number(r.volume.id()));
599 async showMarkDamagedDialog(rows: HoldingsEntry[]) {
600 const copyIds = this.selectedCopyIds(rows, 14 /* ignore damaged */);
602 if (copyIds.length === 0) { return; }
604 let rowsModified = false;
606 const markNext = async(ids: number[]) => {
607 if (ids.length === 0) {
608 return Promise.resolve();
611 this.markDamagedDialog.copyId = ids.pop();
612 return this.markDamagedDialog.open({size: 'lg'}).then(
614 if (ok) { rowsModified = true; }
615 return markNext(ids);
617 dismiss => markNext(ids)
621 await markNext(copyIds);
623 this.refreshHoldings = true;
624 this.holdingsGrid.reload();
628 showMarkMissingDialog(rows: any[]) {
629 const copyIds = this.selectedCopyIds(rows, 4 /* ignore missing */);
630 if (copyIds.length > 0) {
631 this.markMissingDialog.copyIds = copyIds;
632 this.markMissingDialog.open({}).then(
635 this.refreshHoldings = true;
636 this.holdingsGrid.reload();
639 dismissed => {} // avoid console errors
644 // Mark record, library, and potentially the selected call number
645 // as the current transfer target.
646 markLibCnForTransfer(rows: HoldingsEntry[]) {
647 if (rows.length === 0) {
651 // Action may only apply to a single org or volume row.
652 const node = rows[0].treeNode;
653 if (node.nodeType === 'copy') {
659 if (node.nodeType === 'org') {
660 orgId = node.target.id();
662 // Clear volume target when performed on an org unit row
663 this.localStore.removeLocalItem('eg.cat.transfer_target_vol');
665 } else if (node.nodeType === 'volume') {
667 // All volume nodes are children of org nodes.
668 orgId = node.parentNode.target.id();
670 // Add volume target when performed on a volume row.
671 this.localStore.setLocalItem(
672 'eg.cat.transfer_target_vol', node.target.id());
675 this.localStore.setLocalItem('eg.cat.transfer_target_record', this.recordId);
676 this.localStore.setLocalItem('eg.cat.transfer_target_lib', orgId);
679 openAngJsWindow(path: string) {
680 const url = `/eg/staff/${path}`;
681 window.open(url, '_blank');
684 openItemHolds(rows: HoldingsEntry[]) {
685 if (rows.length > 0 && rows[0].copy) {
686 this.openAngJsWindow(`cat/item/${rows[0].copy.id()}/holds`);
690 openItemStatusList(rows: HoldingsEntry[]) {
691 const ids = this.selectedCopyIds(rows);
692 if (ids.length > 0) {
693 return this.openAngJsWindow(`cat/item/search/${ids.join(',')}`);
697 openItemStatus(rows: HoldingsEntry[]) {
698 if (rows.length > 0 && rows[0].copy) {
699 return this.openAngJsWindow(`cat/item/${rows[0].copy.id()}`);
703 openItemTriggeredEvents(rows: HoldingsEntry[]) {
704 if (rows.length > 0 && rows[0].copy) {
705 return this.openAngJsWindow(
706 `cat/item/${rows[0].copy.id()}/triggered_events`);
710 openItemPrintLabels(rows: HoldingsEntry[]) {
711 const ids = this.selectedCopyIds(rows);
712 if (ids.length === 0) { return; }
714 this.anonCache.setItem(null, 'print-labels-these-copies', {copies: ids})
715 .then(key => this.openAngJsWindow(`cat/printlabels/${key}`));
718 openVolCopyEdit(rows: HoldingsEntry[], addVols: boolean, addCopies: boolean) {
720 // The user may select a set of volumes by selecting volume and/or
724 if (r.treeNode.nodeType === 'volume') {
725 volumes.push(r.volume);
726 } else if (r.treeNode.nodeType === 'copy') {
727 volumes.push(r.treeNode.parentNode.target);
731 if (addCopies && !addVols) {
732 // Adding copies to an existing set of volumes.
733 if (volumes.length > 0) {
734 const volIds = volumes.map(v => Number(v.id()));
735 this.holdings.spawnAddHoldingsUi(this.recordId, volIds);
738 } else if (addVols) {
741 if (volumes.length > 0) {
743 // When adding volumes, if any are selected in the grid,
744 // create volumes that have the same label and owner.
746 entries.push({label: v.label(), owner: v.owning_lib()}));
750 // Otherwise create new volumes from scratch.
751 entries.push({owner: this.auth.user().ws_ou()});
754 this.holdings.spawnAddHoldingsUi(
755 this.recordId, null, entries, !addCopies);
759 openItemNotes(rows: HoldingsEntry[], mode: string) {
760 const copyIds = this.selectedCopyIds(rows);
761 if (copyIds.length === 0) { return; }
763 this.copyAlertsDialog.copyIds = copyIds;
764 this.copyAlertsDialog.mode = mode;
765 this.copyAlertsDialog.open({size: 'lg'}).then(
775 openReplaceBarcodeDialog(rows: HoldingsEntry[]) {
776 const ids = this.selectedCopyIds(rows);
777 if (ids.length === 0) { return; }
778 this.replaceBarcode.copyIds = ids;
779 this.replaceBarcode.open({}).then(
789 // mode 'vols' -- only delete empty volumes
790 // mode 'copies' -- only delete selected copies
791 // mode 'both' -- delete selected copies and selected volumes, plus all
792 // copies linked to selected volumes, regardless of whether they are selected.
793 deleteHoldings(rows: HoldingsEntry[], mode: 'vols' | 'copies' | 'both') {
794 const volHash: any = {};
796 if (mode === 'vols' || mode === 'both') {
797 // Collect the volumes to be deleted.
798 rows.filter(r => r.treeNode.nodeType === 'volume').forEach(r => {
799 const vol = this.idl.clone(r.volume);
800 if (mode === 'vols') {
801 if (vol.copies().length > 0) {
802 // cannot delete non-empty volume in this mode.
806 vol.copies().forEach(c => c.isdeleted(true));
809 volHash[vol.id()] = vol;
813 if (mode === 'copies' || mode === 'both') {
814 // Collect the copies to be deleted, including their volumes
815 // since the API expects fleshed volume objects.
816 rows.filter(r => r.treeNode.nodeType === 'copy').forEach(r => {
817 const vol = r.treeNode.parentNode.target;
818 if (!volHash[vol.id()]) {
819 volHash[vol.id()] = this.idl.clone(vol);
820 volHash[vol.id()].copies([]);
822 const copy = this.idl.clone(r.copy);
823 copy.isdeleted(true);
824 volHash[vol.id()].copies().push(copy);
828 if (Object.keys(volHash).length === 0) {
829 // No data to process.
833 // Note forceDeleteCopies should not be necessary here, since we
834 // manually marked all copies as deleted on deleted volumes in
836 this.deleteVolcopy.forceDeleteCopies = mode === 'both';
837 this.deleteVolcopy.volumes = Object.values(volHash);
838 this.deleteVolcopy.open({size: 'sm'}).then(
848 requestItems(rows: HoldingsEntry[]) {
849 const copyIds = this.selectedCopyIds(rows);
850 if (copyIds.length === 0) { return; }
851 const params = {target: copyIds, holdFor: 'staff'};
852 this.router.navigate(['/staff/catalog/hold/C'], {queryParams: params});
855 openBucketDialog(rows: HoldingsEntry[]) {
856 const copyIds = this.selectedCopyIds(rows);
857 if (copyIds.length > 0) {
858 this.bucketDialog.bucketClass = 'copy';
859 this.bucketDialog.itemIds = copyIds;
860 this.bucketDialog.open({size: 'lg'}).then(
861 // No need to reload the grid after adding items to buckets.
868 openConjoinedDialog(rows: HoldingsEntry[]) {
869 const copyIds = this.selectedCopyIds(rows);
870 if (copyIds.length > 0) {
871 this.conjoinedDialog.copyIds = copyIds;
872 this.conjoinedDialog.open({size: 'sm'}).then(
873 ok => {}, // No grid reload required
879 bookItems(rows: HoldingsEntry[]) {
880 const copyIds = this.selectedCopyIds(rows);
881 if (copyIds.length > 0) {
886 makeBookable(rows: HoldingsEntry[]) {
887 const copyIds = this.selectedCopyIds(rows);
888 if (copyIds.length > 0) {
889 this.makeBookableDialog.copyIds = copyIds;
890 this.makeBookableDialog.open({}).then(
891 modified => {}, // No refresh needed