1 import {Component, OnInit, Input, ViewChild, ViewEncapsulation
2 } from '@angular/core';
3 import {Router} from '@angular/router';
4 import {Observable, Observer, of} from 'rxjs';
5 import {map} from 'rxjs/operators';
6 import {Pager} from '@eg/share/util/pager';
7 import {IdlObject, IdlService} from '@eg/core/idl.service';
8 import {StaffCatalogService} from '../catalog.service';
9 import {OrgService} from '@eg/core/org.service';
10 import {PcrudService} from '@eg/core/pcrud.service';
11 import {AuthService} from '@eg/core/auth.service';
12 import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
13 import {GridComponent} from '@eg/share/grid/grid.component';
14 import {GridToolbarCheckboxComponent
15 } from '@eg/share/grid/grid-toolbar-checkbox.component';
16 import {StoreService} from '@eg/core/store.service';
17 import {ServerStoreService} from '@eg/core/server-store.service';
18 import {MarkDamagedDialogComponent
19 } from '@eg/staff/share/holdings/mark-damaged-dialog.component';
20 import {MarkMissingDialogComponent
21 } from '@eg/staff/share/holdings/mark-missing-dialog.component';
22 import {AnonCacheService} from '@eg/share/util/anon-cache.service';
23 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
24 import {CopyAlertsDialogComponent
25 } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
26 import {ReplaceBarcodeDialogComponent
27 } from '@eg/staff/share/holdings/replace-barcode-dialog.component';
28 import {DeleteHoldingDialogComponent
29 } from '@eg/staff/share/holdings/delete-volcopy-dialog.component';
30 import {BucketDialogComponent
31 } from '@eg/staff/share/buckets/bucket-dialog.component';
32 import {ConjoinedItemsDialogComponent
33 } from '@eg/staff/share/holdings/conjoined-items-dialog.component';
34 import {MakeBookableDialogComponent
35 } from '@eg/staff/share/booking/make-bookable-dialog.component';
37 // The holdings grid models a single HoldingsTree, composed of HoldingsTreeNodes
38 // flattened on-demand into a list of HoldingEntry objects.
39 class HoldingsTreeNode {
40 children: HoldingsTreeNode[];
41 nodeType: 'org' | 'callNum' | 'copy';
43 parentNode: HoldingsTreeNode;
53 root: HoldingsTreeNode;
55 this.root = new HoldingsTreeNode();
61 // org unit shortname, call number label, or copy barcode
62 locationLabel: string;
63 // location label indentation depth
64 locationDepth: number | null;
65 callNumCount: number | null;
66 copyCount: number | null;
67 callNumberLabel: string;
71 treeNode: HoldingsTreeNode;
75 selector: 'eg-holdings-maintenance',
76 templateUrl: 'holdings.component.html',
77 styleUrls: ['holdings.component.css'],
78 encapsulation: ViewEncapsulation.None
80 export class HoldingsMaintenanceComponent implements OnInit {
83 gridDataSource: GridDataSource;
84 gridTemplateContext: any;
85 @ViewChild('holdingsGrid', { static: true }) holdingsGrid: GridComponent;
87 // Manage visibility of various sub-sections
88 @ViewChild('callNumsCheckbox', { static: true })
89 private callNumsCheckbox: GridToolbarCheckboxComponent;
90 @ViewChild('copiesCheckbox', { static: true })
91 private copiesCheckbox: GridToolbarCheckboxComponent;
92 @ViewChild('emptyCallNumsCheckbox', { static: true })
93 private emptyCallNumsCheckbox: GridToolbarCheckboxComponent;
94 @ViewChild('emptyLibsCheckbox', { static: true })
95 private emptyLibsCheckbox: GridToolbarCheckboxComponent;
96 @ViewChild('markDamagedDialog', { static: true })
97 private markDamagedDialog: MarkDamagedDialogComponent;
98 @ViewChild('markMissingDialog', { static: true })
99 private markMissingDialog: MarkMissingDialogComponent;
100 @ViewChild('copyAlertsDialog', { static: true })
101 private copyAlertsDialog: CopyAlertsDialogComponent;
102 @ViewChild('replaceBarcode', { static: true })
103 private replaceBarcode: ReplaceBarcodeDialogComponent;
104 @ViewChild('deleteHolding', { static: true })
105 private deleteHolding: DeleteHoldingDialogComponent;
106 @ViewChild('bucketDialog', { static: true })
107 private bucketDialog: BucketDialogComponent;
108 @ViewChild('conjoinedDialog', { static: true })
109 private conjoinedDialog: ConjoinedItemsDialogComponent;
110 @ViewChild('makeBookableDialog', { static: true })
111 private makeBookableDialog: MakeBookableDialogComponent;
113 holdingsTree: HoldingsTree;
115 // nodeType => id => tree node cache
116 treeNodeCache: {[nodeType: string]: {[id: number]: HoldingsTreeNode}};
118 // When true and a grid reload is called, the holdings data will be
119 // re-fetched from the server.
120 refreshHoldings: boolean;
122 // Used as a row identifier in th grid, since we're mixing object types.
125 // List of copies whose due date we need to retrieve.
126 itemCircsNeeded: IdlObject[];
128 // When true draw the grid based on the stored preferences.
129 // When not true, render based on the current "expanded" state of each node.
130 // Rendering from prefs happens on initial load and when any prefs change.
131 renderFromPrefs: boolean;
133 rowClassCallback: (row: any) => string;
134 cellTextGenerator: GridCellTextGenerator;
136 private _recId: number;
137 @Input() set recordId(id: number) {
139 // Only force new data collection when recordId()
140 // is invoked after ngInit() has already run.
145 get recordId(): number {
149 contextOrg: IdlObject;
152 private router: Router,
153 private org: OrgService,
154 private idl: IdlService,
155 private pcrud: PcrudService,
156 private auth: AuthService,
157 private staffCat: StaffCatalogService,
158 private store: ServerStoreService,
159 private localStore: StoreService,
160 private holdings: HoldingsService,
161 private anonCache: AnonCacheService
163 // Set some sane defaults before settings are loaded.
164 this.gridDataSource = new GridDataSource();
165 this.refreshHoldings = true;
166 this.renderFromPrefs = true;
168 // TODO: need a separate setting for this?
169 this.contextOrg = this.staffCat.searchContext.searchOrg;
171 this.rowClassCallback = (row: any): string => {
174 return 'holdings-copy-row';
176 return 'holdings-callNum-row';
179 // Add a generic org unit class and a depth-specific
180 // class for styling different levels of the org tree.
181 return 'holdings-org-row holdings-org-row-' +
182 row.treeNode.target.ou_type().depth();
186 // Text-ify function for cells that use display templates.
187 this.cellTextGenerator = {
188 owner_label: row => row.locationLabel,
189 holdable: row => row.copy ?
190 this.gridTemplateContext.copyIsHoldable(row.copy) : ''
193 this.gridTemplateContext = {
194 toggleExpandRow: (row: HoldingsEntry) => {
195 row.treeNode.expanded = !row.treeNode.expanded;
197 if (!row.treeNode.expanded) {
198 // When collapsing a node, all child nodes should be
199 // collapsed as well.
200 const traverse = (node: HoldingsTreeNode) => {
201 node.expanded = false;
202 node.children.forEach(traverse);
204 traverse(row.treeNode);
207 this.holdingsGrid.reload();
210 copyIsHoldable: (copy: IdlObject): boolean => {
211 return copy.holdable() === 't'
212 && copy.location().holdable() === 't'
213 && copy.status().holdable() === 't';
219 this.initDone = true;
221 // These are pre-cached via the catalog resolver.
222 const settings = this.store.getItemBatchCached([
223 'cat.holdings_show_empty_org',
224 'cat.holdings_show_empty',
225 'cat.holdings_show_copies',
226 'cat.holdings_show_vols'
229 // Show call numbers by default when no preference is set.
230 let showCallNums = settings['cat.holdings_show_vols'];
231 if (showCallNums === null) { showCallNums = true; }
233 this.callNumsCheckbox.checked(showCallNums);
234 this.copiesCheckbox.checked(settings['cat.holdings_show_copies']);
235 this.emptyCallNumsCheckbox.checked(settings['cat.holdings_show_empty']);
236 this.emptyLibsCheckbox.checked(settings['cat.holdings_show_empty_org']);
238 this.initHoldingsTree();
239 this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
240 return this.fetchHoldings(pager);
244 contextOrgChanged(org: IdlObject) {
245 this.contextOrg = org;
250 this.renderFromPrefs = true;
251 this.refreshHoldings = true;
252 this.initHoldingsTree();
253 this.holdingsGrid.reload();
256 toggleShowCopies(value: boolean) {
257 this.store.setItem('cat.holdings_show_copies', value);
259 // Showing copies implies showing call numbers
260 this.callNumsCheckbox.checked(true);
262 this.renderFromPrefs = true;
263 this.holdingsGrid.reload();
266 toggleShowCallNums(value: boolean) {
267 this.store.setItem('cat.holdings_show_vols', value);
269 // Hiding call numbers implies hiding empty call numbers and copies.
270 this.copiesCheckbox.checked(false);
271 this.emptyCallNumsCheckbox.checked(false);
273 this.renderFromPrefs = true;
274 this.holdingsGrid.reload();
277 toggleShowEmptyCallNums(value: boolean) {
278 this.store.setItem('cat.holdings_show_empty', value);
280 this.callNumsCheckbox.checked(true);
282 this.renderFromPrefs = true;
283 this.holdingsGrid.reload();
286 toggleShowEmptyLibs(value: boolean) {
287 this.store.setItem('cat.holdings_show_empty_org', value);
288 this.renderFromPrefs = true;
289 this.holdingsGrid.reload();
292 onRowActivate(row: any) {
294 // Launch copy editor?
296 this.gridTemplateContext.toggleExpandRow(row);
302 const visibleOrgs = this.org.fullPath(this.contextOrg, true);
304 // The initial tree simply matches the org unit tree
305 const traverseOrg = (node: HoldingsTreeNode) => {
306 node.target.children().forEach((org: IdlObject) => {
307 if (visibleOrgs.indexOf(org.id()) === -1) {
308 return; // Org is outside of scope
310 const nodeChild = new HoldingsTreeNode();
311 nodeChild.nodeType = 'org';
312 nodeChild.target = org;
313 nodeChild.parentNode = node;
314 node.children.push(nodeChild);
315 this.treeNodeCache.org[org.id()] = nodeChild;
316 traverseOrg(nodeChild);
320 this.treeNodeCache = {
326 this.holdingsTree = new HoldingsTree();
327 this.holdingsTree.root.nodeType = 'org';
328 this.holdingsTree.root.target = this.org.root();
329 this.treeNodeCache.org[this.org.root().id()] = this.holdingsTree.root;
331 traverseOrg(this.holdingsTree.root);
334 // Org node children are sorted with any child org nodes pushed to the
335 // front, followed by the call number nodes sorted alphabetcially by label.
336 sortOrgNodeChildren(node: HoldingsTreeNode) {
337 node.children = node.children.sort((a, b) => {
338 if (a.nodeType === 'org') {
339 if (b.nodeType === 'org') {
340 return a.target.shortname() < b.target.shortname() ? -1 : 1;
344 } else if (b.nodeType === 'org') {
347 // TODO: should this use label sortkey instead of
348 // the compiled call number label?
349 return a.target._label < b.target._label ? -1 : 1;
354 // Sets call number and copy count sums to nodes that need it.
355 // Applies the initial expansed state of each container node.
356 setTreeCounts(node: HoldingsTreeNode) {
358 if (node.nodeType === 'org') {
360 node.callNumCount = 0;
361 } else if (node.nodeType === 'callNum') {
365 let hasChildOrgWithData = false;
366 let hasChildOrgSansData = false;
367 node.children.forEach(child => {
368 this.setTreeCounts(child);
369 if (node.nodeType === 'org') {
370 node.copyCount += child.copyCount;
371 if (child.nodeType === 'callNum') {
374 hasChildOrgWithData = child.callNumCount > 0;
375 hasChildOrgSansData = child.callNumCount === 0;
376 node.callNumCount += child.callNumCount;
378 } else if (node.nodeType === 'callNum') {
379 node.copyCount = node.children.length;
380 if (this.renderFromPrefs) {
381 node.expanded = this.copiesCheckbox.checked();
386 if (this.renderFromPrefs && node.nodeType === 'org') {
387 if (node.copyCount > 0 && this.callNumsCheckbox.checked()) {
388 node.expanded = true;
389 } else if (node.callNumCount > 0 && this.emptyCallNumsCheckbox.checked()) {
390 node.expanded = true;
391 } else if (hasChildOrgWithData) {
392 node.expanded = true;
393 } else if (hasChildOrgSansData && this.emptyLibsCheckbox.checked()) {
394 node.expanded = true;
396 node.expanded = false;
401 // Create HoldingsEntry objects for tree nodes that should be displayed
402 // and relays them to the grid via the observer.
403 propagateTreeEntries(observer: Observer<HoldingsEntry>, node: HoldingsTreeNode) {
404 const entry = new HoldingsEntry();
405 entry.treeNode = node;
406 entry.index = this.gridIndex++;
408 switch (node.nodeType) {
410 if (node.callNumCount === 0
411 && !this.emptyLibsCheckbox.checked()) {
414 entry.locationLabel = node.target.shortname();
415 entry.locationDepth = node.target.ou_type().depth();
416 entry.copyCount = node.copyCount;
417 entry.callNumCount = node.callNumCount;
418 this.sortOrgNodeChildren(node);
422 if (this.renderFromPrefs) {
423 if (!this.callNumsCheckbox.checked()) {
426 if (node.copyCount === 0
427 && !this.emptyCallNumsCheckbox.checked()) {
431 entry.locationLabel = node.target._label;
432 entry.locationDepth = node.parentNode.target.ou_type().depth() + 1;
433 entry.callNumberLabel = entry.locationLabel;
434 entry.callNum = node.target;
435 entry.copyCount = node.copyCount;
439 entry.locationLabel = node.target.barcode();
440 entry.locationDepth = node.parentNode.parentNode.target.ou_type().depth() + 2;
441 entry.callNumberLabel = node.parentNode.target.label(); // TODO
442 entry.callNum = node.parentNode.target;
443 entry.copy = node.target;
444 entry.circ = node.target._circ;
448 // Tell the grid about the node entry
449 observer.next(entry);
452 // Process the child nodes.
453 node.children.forEach(child =>
454 this.propagateTreeEntries(observer, child));
458 // Turns the tree into a list of entries for grid display
459 flattenHoldingsTree(observer: Observer<HoldingsEntry>) {
461 this.setTreeCounts(this.holdingsTree.root);
462 this.propagateTreeEntries(observer, this.holdingsTree.root);
464 this.renderFromPrefs = false;
467 // Grab call numbers, copies, and related data.
468 fetchHoldings(pager: Pager): Observable<any> {
469 if (!this.recordId) { return of([]); }
471 return new Observable<any>(observer => {
473 if (!this.refreshHoldings) {
474 this.flattenHoldingsTree(observer);
478 this.itemCircsNeeded = [];
480 this.pcrud.search('acn',
481 { record: this.recordId,
482 owning_lib: this.org.fullPath(this.contextOrg, true),
484 label: {'!=' : '##URI##'}
488 acp: ['status', 'location', 'circ_lib', 'parts',
489 'age_protect', 'copy_alerts', 'latest_inventory'],
490 acn: ['prefix', 'suffix', 'copies'],
491 acli: ['inventory_workstation']
494 {authoritative: true}
496 callNum => this.appendCallNum(callNum),
499 this.refreshHoldings = false;
500 this.fetchCircs().then(
501 ok => this.flattenHoldingsTree(observer)
508 // Retrieve circulation objects for checked out items.
509 fetchCircs(): Promise<any> {
510 const copyIds = this.itemCircsNeeded.map(copy => copy.id());
511 if (copyIds.length === 0) { return Promise.resolve(); }
513 return this.pcrud.search('circ', {
514 target_copy: copyIds,
516 }).pipe(map(circ => {
517 const copy = this.itemCircsNeeded.filter(
518 c => Number(c.id()) === Number(circ.target_copy()))[0];
523 // Compile prefix + label + suffix into field callNum._label;
524 setCallNumLabel(callNum: IdlObject) {
525 const pfx = callNum.prefix() ? callNum.prefix().label() : '';
526 const sfx = callNum.suffix() ? callNum.suffix().label() : '';
527 callNum._label = pfx ? pfx + ' ' : '';
528 callNum._label += callNum.label();
529 callNum._label += sfx ? ' ' + sfx : '';
532 // Create the tree node for the call number if it doesn't already exist.
533 // Do the same for its linked copies.
534 appendCallNum(callNum: IdlObject) {
535 let callNumNode = this.treeNodeCache.callNum[callNum.id()];
536 this.setCallNumLabel(callNum);
539 const pNode = this.treeNodeCache.org[callNum.owning_lib()];
540 if (callNumNode.parentNode.target.id() !== pNode.target.id()) {
541 // Call number owning library changed. Un-link it from the
542 // previous org unit collection before adding to the new one.
544 callNumNode.parentNode = pNode;
545 callNumNode.parentNode.children.push(callNumNode);
548 callNumNode = new HoldingsTreeNode();
549 callNumNode.nodeType = 'callNum';
550 callNumNode.parentNode = this.treeNodeCache.org[callNum.owning_lib()];
551 callNumNode.parentNode.children.push(callNumNode);
552 this.treeNodeCache.callNum[callNum.id()] = callNumNode;
555 callNumNode.target = callNum;
558 .filter((copy: IdlObject) => (copy.deleted() !== 't'))
559 .sort((a: IdlObject, b: IdlObject) => a.barcode() < b.barcode() ? -1 : 1)
560 .forEach((copy: IdlObject) => this.appendCopy(callNumNode, copy));
563 // Find or create a copy node.
564 appendCopy(callNumNode: HoldingsTreeNode, copy: IdlObject) {
565 let copyNode = this.treeNodeCache.copy[copy.id()];
568 const oldParent = copyNode.parentNode;
569 if (oldParent.target.id() !== callNumNode.target.id()) {
570 // TODO: copy changed owning call number. Remove it from
571 // the previous call number before adding to the new call number.
572 copyNode.parentNode = callNumNode;
573 callNumNode.children.push(copyNode);
577 copyNode = new HoldingsTreeNode();
578 copyNode.nodeType = 'copy';
579 callNumNode.children.push(copyNode);
580 copyNode.parentNode = callNumNode;
581 this.treeNodeCache.copy[copy.id()] = copyNode;
584 copyNode.target = copy;
585 const stat = Number(copy.status().id());
587 if (stat === 1 /* checked out */ || stat === 16 /* long overdue */) {
588 // Avoid looking up circs on items that are not checked out.
589 this.itemCircsNeeded.push(copy);
593 // Which copies in the grid are selected.
594 selectedCopyIds(rows: HoldingsEntry[], skipStatus?: number): number[] {
595 let copyRows = rows.filter(r => Boolean(r.copy)).map(r => r.copy);
597 copyRows = copyRows.filter(
598 c => Number(c.status().id()) !== Number(skipStatus));
600 return copyRows.map(c => Number(c.id()));
603 selectedCallNumIds(rows: HoldingsEntry[]): number[] {
605 .filter(r => r.treeNode.nodeType === 'callNum')
606 .map(r => Number(r.callNum.id()));
609 async showMarkDamagedDialog(rows: HoldingsEntry[]) {
610 const copyIds = this.selectedCopyIds(rows, 14 /* ignore damaged */);
612 if (copyIds.length === 0) { return; }
614 let rowsModified = false;
616 const markNext = async(ids: number[]) => {
617 if (ids.length === 0) {
618 return Promise.resolve();
621 this.markDamagedDialog.copyId = ids.pop();
622 return this.markDamagedDialog.open({size: 'lg'}).subscribe(
624 if (ok) { rowsModified = true; }
625 return markNext(ids);
627 dismiss => markNext(ids)
631 await markNext(copyIds);
633 this.refreshHoldings = true;
634 this.holdingsGrid.reload();
638 showMarkMissingDialog(rows: any[]) {
639 const copyIds = this.selectedCopyIds(rows, 4 /* ignore missing */);
640 if (copyIds.length > 0) {
641 this.markMissingDialog.copyIds = copyIds;
642 this.markMissingDialog.open({}).subscribe(
645 this.refreshHoldings = true;
646 this.holdingsGrid.reload();
649 dismissed => {} // avoid console errors
654 // Mark record, library, and potentially the selected call number
655 // as the current transfer target.
656 markLibCnForTransfer(rows: HoldingsEntry[]) {
657 if (rows.length === 0) {
661 // Action may only apply to a single org or call number row.
662 const node = rows[0].treeNode;
663 if (node.nodeType === 'copy') {
669 if (node.nodeType === 'org') {
670 orgId = node.target.id();
672 // Clear call number target when performed on an org unit row
673 this.localStore.removeLocalItem('eg.cat.transfer_target_vol');
675 } else if (node.nodeType === 'callNum') {
677 // All call number nodes are children of org nodes.
678 orgId = node.parentNode.target.id();
680 // Add call number target when performed on a call number row.
681 this.localStore.setLocalItem(
682 'eg.cat.transfer_target_vol', node.target.id());
685 this.localStore.setLocalItem('eg.cat.transfer_target_record', this.recordId);
686 this.localStore.setLocalItem('eg.cat.transfer_target_lib', orgId);
689 openAngJsWindow(path: string) {
690 const url = `/eg/staff/${path}`;
691 window.open(url, '_blank');
694 openItemHolds(rows: HoldingsEntry[]) {
695 if (rows.length > 0 && rows[0].copy) {
696 this.openAngJsWindow(`cat/item/${rows[0].copy.id()}/holds`);
700 openItemStatusList(rows: HoldingsEntry[]) {
701 const ids = this.selectedCopyIds(rows);
702 if (ids.length > 0) {
703 return this.openAngJsWindow(`cat/item/search/${ids.join(',')}`);
707 openItemStatus(rows: HoldingsEntry[]) {
708 if (rows.length > 0 && rows[0].copy) {
709 return this.openAngJsWindow(`cat/item/${rows[0].copy.id()}`);
713 openItemTriggeredEvents(rows: HoldingsEntry[]) {
714 if (rows.length > 0 && rows[0].copy) {
715 return this.openAngJsWindow(
716 `cat/item/${rows[0].copy.id()}/triggered_events`);
720 openItemPrintLabels(rows: HoldingsEntry[]) {
721 const ids = this.selectedCopyIds(rows);
722 if (ids.length === 0) { return; }
724 this.anonCache.setItem(null, 'print-labels-these-copies', {copies: ids})
725 .then(key => this.openAngJsWindow(`cat/printlabels/${key}`));
728 openHoldingEdit(rows: HoldingsEntry[], addCallNums: boolean, addCopies: boolean) {
730 // The user may select a set of call numbers by selecting call number and/or
734 if (r.treeNode.nodeType === 'callNum') {
735 callNums.push(r.callNum);
736 } else if (r.treeNode.nodeType === 'copy') {
737 callNums.push(r.treeNode.parentNode.target);
741 if (addCopies && !addCallNums) {
742 // Adding copies to an existing set of call numbers.
743 if (callNums.length > 0) {
744 const callNumIds = callNums.map(v => Number(v.id()));
745 this.holdings.spawnAddHoldingsUi(this.recordId, callNumIds);
748 } else if (addCallNums) {
751 if (callNums.length > 0) {
753 // When adding call numbers, if any are selected in the grid,
754 // create call numbers that have the same label and owner.
755 callNums.forEach(v =>
756 entries.push({label: v.label(), owner: v.owning_lib()}));
760 // Otherwise create new call numbers from scratch.
761 entries.push({owner: this.auth.user().ws_ou()});
764 this.holdings.spawnAddHoldingsUi(
765 this.recordId, null, entries, !addCopies);
769 openItemNotes(rows: HoldingsEntry[], mode: string) {
770 const copyIds = this.selectedCopyIds(rows);
771 if (copyIds.length === 0) { return; }
773 this.copyAlertsDialog.copyIds = copyIds;
774 this.copyAlertsDialog.mode = mode;
775 this.copyAlertsDialog.open({size: 'lg'}).subscribe(
784 openReplaceBarcodeDialog(rows: HoldingsEntry[]) {
785 const ids = this.selectedCopyIds(rows);
786 if (ids.length === 0) { return; }
787 this.replaceBarcode.copyIds = ids;
788 this.replaceBarcode.open({}).subscribe(
797 // mode 'callNums' -- only delete empty call numbers
798 // mode 'copies' -- only delete selected copies
799 // mode 'both' -- delete selected copies and selected call numbers, plus all
800 // copies linked to selected call numbers, regardless of whether they are selected.
801 deleteHoldings(rows: HoldingsEntry[], mode: 'callNums' | 'copies' | 'both') {
802 const callNumHash: any = {};
804 if (mode === 'callNums' || mode === 'both') {
805 // Collect the call numbers to be deleted.
806 rows.filter(r => r.treeNode.nodeType === 'callNum').forEach(r => {
807 const callNum = this.idl.clone(r.callNum);
808 if (mode === 'callNums') {
809 if (callNum.copies().length > 0) {
810 // cannot delete non-empty call number in this mode.
814 callNum.copies().forEach(c => c.isdeleted(true));
816 callNum.isdeleted(true);
817 callNumHash[callNum.id()] = callNum;
821 if (mode === 'copies' || mode === 'both') {
822 // Collect the copies to be deleted, including their call numbers
823 // since the API expects fleshed call number objects.
824 rows.filter(r => r.treeNode.nodeType === 'copy').forEach(r => {
825 const callNum = r.treeNode.parentNode.target;
826 if (!callNumHash[callNum.id()]) {
827 callNumHash[callNum.id()] = this.idl.clone(callNum);
828 callNumHash[callNum.id()].copies([]);
830 const copy = this.idl.clone(r.copy);
831 copy.isdeleted(true);
832 callNumHash[callNum.id()].copies().push(copy);
836 if (Object.keys(callNumHash).length === 0) {
837 // No data to process.
841 // Note forceDeleteCopies should not be necessary here, since we
842 // manually marked all copies as deleted on deleted call numbers in
844 this.deleteHolding.forceDeleteCopies = mode === 'both';
845 this.deleteHolding.callNums = Object.values(callNumHash);
846 this.deleteHolding.open({size: 'sm'}).subscribe(
855 requestItems(rows: HoldingsEntry[]) {
856 const copyIds = this.selectedCopyIds(rows);
857 if (copyIds.length === 0) { return; }
858 const params = {target: copyIds, holdFor: 'staff'};
859 this.router.navigate(['/staff/catalog/hold/C'], {queryParams: params});
862 openBucketDialog(rows: HoldingsEntry[]) {
863 const copyIds = this.selectedCopyIds(rows);
864 if (copyIds.length > 0) {
865 this.bucketDialog.bucketClass = 'copy';
866 this.bucketDialog.itemIds = copyIds;
867 this.bucketDialog.open({size: 'lg'});
871 openConjoinedDialog(rows: HoldingsEntry[]) {
872 const copyIds = this.selectedCopyIds(rows);
873 if (copyIds.length > 0) {
874 this.conjoinedDialog.copyIds = copyIds;
875 this.conjoinedDialog.open({size: 'sm'});
879 bookItems(rows: HoldingsEntry[]) {
880 const copyIds = this.selectedCopyIds(rows);
881 if (copyIds.length > 0) {
882 this.router.navigate(
883 ['staff', 'booking', 'create_reservation', 'for_resource', rows.filter(r => Boolean(r.copy))[0].copy.barcode()]
888 makeBookable(rows: HoldingsEntry[]) {
889 const copyIds = this.selectedCopyIds(rows);
890 if (copyIds.length > 0) {
891 this.makeBookableDialog.copyIds = copyIds;
892 this.makeBookableDialog.open({});
896 manageReservations(rows: HoldingsEntry[]) {
897 const copyIds = this.selectedCopyIds(rows);
898 if (copyIds.length > 0) {
899 this.router.navigate(
900 ['staff', 'booking', 'manage_reservations', 'by_resource', rows.filter(r => Boolean(r.copy))[0].copy.barcode()]