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';
36 import {TransferItemsComponent
37 } from '@eg/staff/share/holdings/transfer-items.component';
38 import {TransferHoldingsComponent
39 } from '@eg/staff/share/holdings/transfer-holdings.component';
40 import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
43 // The holdings grid models a single HoldingsTree, composed of HoldingsTreeNodes
44 // flattened on-demand into a list of HoldingEntry objects.
45 class HoldingsTreeNode {
46 children: HoldingsTreeNode[];
47 nodeType: 'org' | 'callNum' | 'copy';
49 parentNode: HoldingsTreeNode;
59 root: HoldingsTreeNode;
61 this.root = new HoldingsTreeNode();
67 // org unit shortname, call number label, or copy barcode
68 locationLabel: string;
69 // location label indentation depth
70 locationDepth: number | null;
71 callNumCount: number | null;
72 copyCount: number | null;
73 callNumberLabel: string;
77 treeNode: HoldingsTreeNode;
81 selector: 'eg-holdings-maintenance',
82 templateUrl: 'holdings.component.html',
83 styleUrls: ['holdings.component.css'],
84 encapsulation: ViewEncapsulation.None
86 export class HoldingsMaintenanceComponent implements OnInit {
89 gridDataSource: GridDataSource;
90 gridTemplateContext: any;
91 @ViewChild('holdingsGrid', { static: true }) holdingsGrid: GridComponent;
93 // Manage visibility of various sub-sections
94 @ViewChild('callNumsCheckbox', { static: true })
95 private callNumsCheckbox: GridToolbarCheckboxComponent;
96 @ViewChild('copiesCheckbox', { static: true })
97 private copiesCheckbox: GridToolbarCheckboxComponent;
98 @ViewChild('emptyCallNumsCheckbox', { static: true })
99 private emptyCallNumsCheckbox: GridToolbarCheckboxComponent;
100 @ViewChild('emptyLibsCheckbox', { static: true })
101 private emptyLibsCheckbox: GridToolbarCheckboxComponent;
102 @ViewChild('markDamagedDialog', { static: true })
103 private markDamagedDialog: MarkDamagedDialogComponent;
104 @ViewChild('markMissingDialog', { static: true })
105 private markMissingDialog: MarkMissingDialogComponent;
106 @ViewChild('copyAlertsDialog', { static: true })
107 private copyAlertsDialog: CopyAlertsDialogComponent;
108 @ViewChild('replaceBarcode', { static: true })
109 private replaceBarcode: ReplaceBarcodeDialogComponent;
110 @ViewChild('deleteHolding', { static: true })
111 private deleteHolding: DeleteHoldingDialogComponent;
112 @ViewChild('bucketDialog', { static: true })
113 private bucketDialog: BucketDialogComponent;
114 @ViewChild('conjoinedDialog', { static: true })
115 private conjoinedDialog: ConjoinedItemsDialogComponent;
116 @ViewChild('makeBookableDialog', { static: true })
117 private makeBookableDialog: MakeBookableDialogComponent;
118 @ViewChild('transferItems', {static: false})
119 private transferItems: TransferItemsComponent;
120 @ViewChild('transferHoldings', {static: false})
121 private transferHoldings: TransferHoldingsComponent;
122 @ViewChild('transferAlert', {static: false})
123 private transferAlert: AlertDialogComponent;
125 holdingsTree: HoldingsTree;
127 // nodeType => id => tree node cache
128 treeNodeCache: {[nodeType: string]: {[id: number]: HoldingsTreeNode}};
130 // When true and a grid reload is called, the holdings data will be
131 // re-fetched from the server.
132 refreshHoldings: boolean;
134 // Used as a row identifier in th grid, since we're mixing object types.
137 // List of copies whose due date we need to retrieve.
138 itemCircsNeeded: IdlObject[];
140 // When true draw the grid based on the stored preferences.
141 // When not true, render based on the current "expanded" state of each node.
142 // Rendering from prefs happens on initial load and when any prefs change.
143 renderFromPrefs: boolean;
145 rowClassCallback: (row: any) => string;
146 cellTextGenerator: GridCellTextGenerator;
148 private _recId: number;
149 @Input() set recordId(id: number) {
151 // Only force new data collection when recordId()
152 // is invoked after ngInit() has already run.
157 get recordId(): number {
161 contextOrg: IdlObject;
164 private router: Router,
165 private org: OrgService,
166 private idl: IdlService,
167 private pcrud: PcrudService,
168 private auth: AuthService,
169 private staffCat: StaffCatalogService,
170 private store: ServerStoreService,
171 private localStore: StoreService,
172 private holdings: HoldingsService,
173 private anonCache: AnonCacheService
175 // Set some sane defaults before settings are loaded.
176 this.gridDataSource = new GridDataSource();
177 this.refreshHoldings = true;
178 this.renderFromPrefs = true;
180 // TODO: need a separate setting for this?
181 this.contextOrg = this.staffCat.searchContext.searchOrg;
183 this.rowClassCallback = (row: any): string => {
186 return 'holdings-copy-row';
188 return 'holdings-callNum-row';
191 // Add a generic org unit class and a depth-specific
192 // class for styling different levels of the org tree.
193 return 'holdings-org-row holdings-org-row-' +
194 row.treeNode.target.ou_type().depth();
198 // Text-ify function for cells that use display templates.
199 this.cellTextGenerator = {
200 owner_label: row => row.locationLabel,
201 holdable: row => row.copy ?
202 this.gridTemplateContext.copyIsHoldable(row.copy) : ''
205 this.gridTemplateContext = {
206 toggleExpandRow: (row: HoldingsEntry) => {
207 row.treeNode.expanded = !row.treeNode.expanded;
209 if (!row.treeNode.expanded) {
210 // When collapsing a node, all child nodes should be
211 // collapsed as well.
212 const traverse = (node: HoldingsTreeNode) => {
213 node.expanded = false;
214 node.children.forEach(traverse);
216 traverse(row.treeNode);
219 this.holdingsGrid.reload();
222 copyIsHoldable: (copy: IdlObject): boolean => {
223 return copy.holdable() === 't'
224 && copy.location().holdable() === 't'
225 && copy.status().holdable() === 't';
231 this.initDone = true;
233 // These are pre-cached via the catalog resolver.
234 const settings = this.store.getItemBatchCached([
235 'cat.holdings_show_empty_org',
236 'cat.holdings_show_empty',
237 'cat.holdings_show_copies',
238 'cat.holdings_show_vols'
241 // Show call numbers by default when no preference is set.
242 let showCallNums = settings['cat.holdings_show_vols'];
243 if (showCallNums === null) { showCallNums = true; }
245 this.callNumsCheckbox.checked(showCallNums);
246 this.copiesCheckbox.checked(settings['cat.holdings_show_copies']);
247 this.emptyCallNumsCheckbox.checked(settings['cat.holdings_show_empty']);
248 this.emptyLibsCheckbox.checked(settings['cat.holdings_show_empty_org']);
250 this.initHoldingsTree();
251 this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
252 return this.fetchHoldings(pager);
256 contextOrgChanged(org: IdlObject) {
257 this.contextOrg = org;
262 this.renderFromPrefs = true;
263 this.refreshHoldings = true;
264 this.initHoldingsTree();
265 this.holdingsGrid.reload();
268 toggleShowCopies(value: boolean) {
269 this.store.setItem('cat.holdings_show_copies', value);
271 // Showing copies implies showing call numbers
272 this.callNumsCheckbox.checked(true);
274 this.renderFromPrefs = true;
275 this.holdingsGrid.reload();
278 toggleShowCallNums(value: boolean) {
279 this.store.setItem('cat.holdings_show_vols', value);
281 // Hiding call numbers implies hiding empty call numbers and copies.
282 this.copiesCheckbox.checked(false);
283 this.emptyCallNumsCheckbox.checked(false);
285 this.renderFromPrefs = true;
286 this.holdingsGrid.reload();
289 toggleShowEmptyCallNums(value: boolean) {
290 this.store.setItem('cat.holdings_show_empty', value);
292 this.callNumsCheckbox.checked(true);
294 this.renderFromPrefs = true;
295 this.holdingsGrid.reload();
298 toggleShowEmptyLibs(value: boolean) {
299 this.store.setItem('cat.holdings_show_empty_org', value);
300 this.renderFromPrefs = true;
301 this.holdingsGrid.reload();
304 onRowActivate(row: any) {
306 // Launch copy editor?
308 this.gridTemplateContext.toggleExpandRow(row);
314 const visibleOrgs = this.org.fullPath(this.contextOrg, true);
316 // The initial tree simply matches the org unit tree
317 const traverseOrg = (node: HoldingsTreeNode) => {
318 node.target.children().forEach((org: IdlObject) => {
319 if (visibleOrgs.indexOf(org.id()) === -1) {
320 return; // Org is outside of scope
322 const nodeChild = new HoldingsTreeNode();
323 nodeChild.nodeType = 'org';
324 nodeChild.target = org;
325 nodeChild.parentNode = node;
326 node.children.push(nodeChild);
327 this.treeNodeCache.org[org.id()] = nodeChild;
328 traverseOrg(nodeChild);
332 this.treeNodeCache = {
338 this.holdingsTree = new HoldingsTree();
339 this.holdingsTree.root.nodeType = 'org';
340 this.holdingsTree.root.target = this.org.root();
341 this.treeNodeCache.org[this.org.root().id()] = this.holdingsTree.root;
343 traverseOrg(this.holdingsTree.root);
346 // Org node children are sorted with any child org nodes pushed to the
347 // front, followed by the call number nodes sorted alphabetcially by label.
348 sortOrgNodeChildren(node: HoldingsTreeNode) {
349 node.children = node.children.sort((a, b) => {
350 if (a.nodeType === 'org') {
351 if (b.nodeType === 'org') {
352 return a.target.shortname() < b.target.shortname() ? -1 : 1;
356 } else if (b.nodeType === 'org') {
359 // TODO: should this use label sortkey instead of
360 // the compiled call number label?
361 return a.target._label < b.target._label ? -1 : 1;
366 // Sets call number and copy count sums to nodes that need it.
367 // Applies the initial expansed state of each container node.
368 setTreeCounts(node: HoldingsTreeNode) {
370 if (node.nodeType === 'org') {
372 node.callNumCount = 0;
373 } else if (node.nodeType === 'callNum') {
377 let hasChildOrgWithData = false;
378 let hasChildOrgSansData = false;
379 node.children.forEach(child => {
380 this.setTreeCounts(child);
381 if (node.nodeType === 'org') {
382 node.copyCount += child.copyCount;
383 if (child.nodeType === 'callNum') {
386 hasChildOrgWithData = child.callNumCount > 0;
387 hasChildOrgSansData = child.callNumCount === 0;
388 node.callNumCount += child.callNumCount;
390 } else if (node.nodeType === 'callNum') {
391 node.copyCount = node.children.length;
392 if (this.renderFromPrefs) {
393 node.expanded = this.copiesCheckbox.checked();
398 if (this.renderFromPrefs && node.nodeType === 'org') {
399 if (node.copyCount > 0 && this.callNumsCheckbox.checked()) {
400 node.expanded = true;
401 } else if (node.callNumCount > 0 && this.emptyCallNumsCheckbox.checked()) {
402 node.expanded = true;
403 } else if (hasChildOrgWithData) {
404 node.expanded = true;
405 } else if (hasChildOrgSansData && this.emptyLibsCheckbox.checked()) {
406 node.expanded = true;
408 node.expanded = false;
413 // Create HoldingsEntry objects for tree nodes that should be displayed
414 // and relays them to the grid via the observer.
415 propagateTreeEntries(observer: Observer<HoldingsEntry>, node: HoldingsTreeNode) {
416 const entry = new HoldingsEntry();
417 entry.treeNode = node;
418 entry.index = this.gridIndex++;
420 switch (node.nodeType) {
422 if (node.callNumCount === 0
423 && !this.emptyLibsCheckbox.checked()) {
426 entry.locationLabel = node.target.shortname();
427 entry.locationDepth = node.target.ou_type().depth();
428 entry.copyCount = node.copyCount;
429 entry.callNumCount = node.callNumCount;
430 this.sortOrgNodeChildren(node);
434 if (this.renderFromPrefs) {
435 if (!this.callNumsCheckbox.checked()) {
438 if (node.copyCount === 0
439 && !this.emptyCallNumsCheckbox.checked()) {
443 entry.locationLabel = node.target._label;
444 entry.locationDepth = node.parentNode.target.ou_type().depth() + 1;
445 entry.callNumberLabel = entry.locationLabel;
446 entry.callNum = node.target;
447 entry.copyCount = node.copyCount;
451 entry.locationLabel = node.target.barcode();
452 entry.locationDepth = node.parentNode.parentNode.target.ou_type().depth() + 2;
453 entry.callNumberLabel = node.parentNode.target.label(); // TODO
454 entry.callNum = node.parentNode.target;
455 entry.copy = node.target;
456 entry.circ = node.target._circ;
460 // Tell the grid about the node entry
461 observer.next(entry);
464 // Process the child nodes.
465 node.children.forEach(child =>
466 this.propagateTreeEntries(observer, child));
470 // Turns the tree into a list of entries for grid display
471 flattenHoldingsTree(observer: Observer<HoldingsEntry>) {
473 this.setTreeCounts(this.holdingsTree.root);
474 this.propagateTreeEntries(observer, this.holdingsTree.root);
476 this.renderFromPrefs = false;
479 // Grab call numbers, copies, and related data.
480 fetchHoldings(pager: Pager): Observable<any> {
481 if (!this.recordId) { return of([]); }
483 return new Observable<any>(observer => {
485 if (!this.refreshHoldings) {
486 this.flattenHoldingsTree(observer);
490 this.itemCircsNeeded = [];
492 this.pcrud.search('acn',
493 { record: this.recordId,
494 owning_lib: this.org.fullPath(this.contextOrg, true),
496 label: {'!=' : '##URI##'}
500 acp: ['status', 'location', 'circ_lib', 'parts',
501 'age_protect', 'copy_alerts', 'latest_inventory'],
502 acn: ['prefix', 'suffix', 'copies'],
503 acli: ['inventory_workstation']
506 {authoritative: true}
508 callNum => this.appendCallNum(callNum),
511 this.refreshHoldings = false;
512 this.fetchCircs().then(
513 ok => this.flattenHoldingsTree(observer)
520 // Retrieve circulation objects for checked out items.
521 fetchCircs(): Promise<any> {
522 const copyIds = this.itemCircsNeeded.map(copy => copy.id());
523 if (copyIds.length === 0) { return Promise.resolve(); }
525 return this.pcrud.search('circ', {
526 target_copy: copyIds,
528 }).pipe(map(circ => {
529 const copy = this.itemCircsNeeded.filter(
530 c => Number(c.id()) === Number(circ.target_copy()))[0];
535 // Compile prefix + label + suffix into field callNum._label;
536 setCallNumLabel(callNum: IdlObject) {
537 const pfx = callNum.prefix() ? callNum.prefix().label() : '';
538 const sfx = callNum.suffix() ? callNum.suffix().label() : '';
539 callNum._label = pfx ? pfx + ' ' : '';
540 callNum._label += callNum.label();
541 callNum._label += sfx ? ' ' + sfx : '';
544 // Create the tree node for the call number if it doesn't already exist.
545 // Do the same for its linked copies.
546 appendCallNum(callNum: IdlObject) {
547 let callNumNode = this.treeNodeCache.callNum[callNum.id()];
548 this.setCallNumLabel(callNum);
551 const pNode = this.treeNodeCache.org[callNum.owning_lib()];
552 if (callNumNode.parentNode.target.id() !== pNode.target.id()) {
553 // Call number owning library changed. Un-link it from the
554 // previous org unit collection before adding to the new one.
556 callNumNode.parentNode = pNode;
557 callNumNode.parentNode.children.push(callNumNode);
560 callNumNode = new HoldingsTreeNode();
561 callNumNode.nodeType = 'callNum';
562 callNumNode.parentNode = this.treeNodeCache.org[callNum.owning_lib()];
563 callNumNode.parentNode.children.push(callNumNode);
564 this.treeNodeCache.callNum[callNum.id()] = callNumNode;
567 callNumNode.target = callNum;
570 .filter((copy: IdlObject) => (copy.deleted() !== 't'))
571 .sort((a: IdlObject, b: IdlObject) => a.barcode() < b.barcode() ? -1 : 1)
572 .forEach((copy: IdlObject) => this.appendCopy(callNumNode, copy));
575 // Find or create a copy node.
576 appendCopy(callNumNode: HoldingsTreeNode, copy: IdlObject) {
577 let copyNode = this.treeNodeCache.copy[copy.id()];
580 const oldParent = copyNode.parentNode;
581 if (oldParent.target.id() !== callNumNode.target.id()) {
582 // TODO: copy changed owning call number. Remove it from
583 // the previous call number before adding to the new call number.
584 copyNode.parentNode = callNumNode;
585 callNumNode.children.push(copyNode);
589 copyNode = new HoldingsTreeNode();
590 copyNode.nodeType = 'copy';
591 callNumNode.children.push(copyNode);
592 copyNode.parentNode = callNumNode;
593 this.treeNodeCache.copy[copy.id()] = copyNode;
596 copyNode.target = copy;
597 const stat = Number(copy.status().id());
599 if (stat === 1 /* checked out */ || stat === 16 /* long overdue */) {
600 // Avoid looking up circs on items that are not checked out.
601 this.itemCircsNeeded.push(copy);
605 // Which copies in the grid are selected.
606 selectedCopyIds(rows: HoldingsEntry[], skipStatus?: number): number[] {
607 return this.selectedCopies(rows, skipStatus).map(c => Number(c.id()));
610 selectedCopies(rows: HoldingsEntry[], skipStatus?: number): IdlObject[] {
611 let copyRows = rows.filter(r => Boolean(r.copy)).map(r => r.copy);
613 copyRows = copyRows.filter(
614 c => Number(c.status().id()) !== Number(skipStatus));
619 selectedCallNumIds(rows: HoldingsEntry[]): number[] {
620 return this.selectedCallNums(rows).map(cn => cn.id());
623 selectedCallNums(rows: HoldingsEntry[]): IdlObject[] {
625 .filter(r => r.treeNode.nodeType === 'callNum')
626 .map(r => r.callNum);
630 async showMarkDamagedDialog(rows: HoldingsEntry[]) {
631 const copyIds = this.selectedCopyIds(rows, 14 /* ignore damaged */);
633 if (copyIds.length === 0) { return; }
635 let rowsModified = false;
637 const markNext = async(ids: number[]) => {
638 if (ids.length === 0) {
639 return Promise.resolve();
642 this.markDamagedDialog.copyId = ids.pop();
643 return this.markDamagedDialog.open({size: 'lg'}).subscribe(
645 if (ok) { rowsModified = true; }
646 return markNext(ids);
648 dismiss => markNext(ids)
652 await markNext(copyIds);
654 this.refreshHoldings = true;
655 this.holdingsGrid.reload();
659 showMarkMissingDialog(rows: any[]) {
660 const copyIds = this.selectedCopyIds(rows, 4 /* ignore missing */);
661 if (copyIds.length > 0) {
662 this.markMissingDialog.copyIds = copyIds;
663 this.markMissingDialog.open({}).subscribe(
666 this.refreshHoldings = true;
667 this.holdingsGrid.reload();
670 dismissed => {} // avoid console errors
675 // Mark record, library, and potentially the selected call number
676 // as the current transfer target.
677 markLibCnForTransfer(rows: HoldingsEntry[]) {
678 if (rows.length === 0) {
682 // Action may only apply to a single org or call number row.
683 const node = rows[0].treeNode;
684 if (node.nodeType === 'copy') { return; }
688 if (node.nodeType === 'org') {
689 orgId = node.target.id();
691 // Clear call number target when performed on an org unit row
692 this.localStore.removeLocalItem('eg.cat.transfer_target_vol');
694 } else if (node.nodeType === 'callNum') {
696 // All call number nodes are children of org nodes.
697 orgId = node.parentNode.target.id();
699 // Add call number target when performed on a call number row.
700 this.localStore.setLocalItem(
701 'eg.cat.transfer_target_vol', node.target.id());
704 // Track lib and record to support transfering items from
705 // a different bib record to this record at the selected
707 this.localStore.setLocalItem('eg.cat.transfer_target_lib', orgId);
708 this.localStore.setLocalItem('eg.cat.transfer_target_record', this.recordId);
711 openAngJsWindow(path: string) {
712 const url = `/eg/staff/${path}`;
713 window.open(url, '_blank');
716 openItemHolds(rows: HoldingsEntry[]) {
717 if (rows.length > 0 && rows[0].copy) {
718 this.openAngJsWindow(`cat/item/${rows[0].copy.id()}/holds`);
722 openItemStatusList(rows: HoldingsEntry[]) {
723 const ids = this.selectedCopyIds(rows);
724 if (ids.length > 0) {
725 return this.openAngJsWindow(`cat/item/search/${ids.join(',')}`);
729 openItemStatus(rows: HoldingsEntry[]) {
730 if (rows.length > 0 && rows[0].copy) {
731 return this.openAngJsWindow(`cat/item/${rows[0].copy.id()}`);
735 openItemTriggeredEvents(rows: HoldingsEntry[]) {
736 if (rows.length > 0 && rows[0].copy) {
737 return this.openAngJsWindow(
738 `cat/item/${rows[0].copy.id()}/triggered_events`);
742 openItemPrintLabels(rows: HoldingsEntry[]) {
743 const ids = this.selectedCopyIds(rows);
744 if (ids.length === 0) { return; }
746 this.anonCache.setItem(null, 'print-labels-these-copies', {copies: ids})
747 .then(key => this.openAngJsWindow(`cat/printlabels/${key}`));
750 openHoldingEdit(rows: HoldingsEntry[], addCallNums: boolean, addCopies: boolean) {
752 // The user may select a set of call numbers by selecting call
753 // number and/or item rows. Owning libs for new call numbers may
754 // also come from org unit row selection.
758 if (r.treeNode.nodeType === 'callNum') {
759 callNums.push(r.callNum);
761 } else if (r.treeNode.nodeType === 'copy') {
762 callNums.push(r.treeNode.parentNode.target);
764 } else if (r.treeNode.nodeType === 'org') {
765 orgs[r.treeNode.target.id()] = true;
769 if (addCopies && !addCallNums) {
770 // Adding copies to an existing set of call numbers.
771 if (callNums.length > 0) {
772 const callNumIds = callNums.map(v => Number(v.id()));
773 this.holdings.spawnAddHoldingsUi(this.recordId, callNumIds);
776 } else if (addCallNums) {
779 // Use selected call numbers as basis for new call numbers.
780 callNums.forEach(v =>
781 entries.push({label: v.label(), owner: v.owning_lib()}));
783 // Use selected org units as owning libs for new call numbers
784 Object.keys(orgs).forEach(id => entries.push({owner: id}));
786 if (entries.length === 0) {
787 // Otherwise create new call numbers for "here"
788 entries.push({owner: this.auth.user().ws_ou()});
791 this.holdings.spawnAddHoldingsUi(
792 this.recordId, null, entries, !addCopies);
796 openItemNotes(rows: HoldingsEntry[], mode: string) {
797 const copyIds = this.selectedCopyIds(rows);
798 if (copyIds.length === 0) { return; }
800 this.copyAlertsDialog.copyIds = copyIds;
801 this.copyAlertsDialog.mode = mode;
802 this.copyAlertsDialog.open({size: 'lg'}).subscribe(
811 openReplaceBarcodeDialog(rows: HoldingsEntry[]) {
812 const ids = this.selectedCopyIds(rows);
813 if (ids.length === 0) { return; }
814 this.replaceBarcode.copyIds = ids;
815 this.replaceBarcode.open({}).subscribe(
824 // mode 'callNums' -- only delete empty call numbers
825 // mode 'copies' -- only delete selected copies
826 // mode 'both' -- delete selected copies and selected call numbers, plus all
827 // copies linked to selected call numbers, regardless of whether they are selected.
828 deleteHoldings(rows: HoldingsEntry[], mode: 'callNums' | 'copies' | 'both') {
829 const callNumHash: any = {};
831 if (mode === 'callNums' || mode === 'both') {
832 // Collect the call numbers to be deleted.
833 rows.filter(r => r.treeNode.nodeType === 'callNum').forEach(r => {
834 const callNum = this.idl.clone(r.callNum);
835 if (mode === 'callNums') {
836 if (callNum.copies().length > 0) {
837 // cannot delete non-empty call number in this mode.
841 callNum.copies().forEach(c => c.isdeleted(true));
843 callNum.isdeleted(true);
844 callNumHash[callNum.id()] = callNum;
848 if (mode === 'copies' || mode === 'both') {
849 // Collect the copies to be deleted, including their call numbers
850 // since the API expects fleshed call number objects.
851 rows.filter(r => r.treeNode.nodeType === 'copy').forEach(r => {
852 const callNum = r.treeNode.parentNode.target;
853 if (!callNumHash[callNum.id()]) {
854 callNumHash[callNum.id()] = this.idl.clone(callNum);
855 callNumHash[callNum.id()].copies([]);
857 const copy = this.idl.clone(r.copy);
858 copy.isdeleted(true);
859 callNumHash[callNum.id()].copies().push(copy);
863 if (Object.keys(callNumHash).length === 0) {
864 // No data to process.
868 // Note forceDeleteCopies should not be necessary here, since we
869 // manually marked all copies as deleted on deleted call numbers in
871 this.deleteHolding.forceDeleteCopies = mode === 'both';
872 this.deleteHolding.callNums = Object.values(callNumHash);
873 this.deleteHolding.open({size: 'sm'}).subscribe(
882 requestItems(rows: HoldingsEntry[]) {
883 const copyIds = this.selectedCopyIds(rows);
884 if (copyIds.length === 0) { return; }
885 const params = {target: copyIds, holdFor: 'staff'};
886 this.router.navigate(['/staff/catalog/hold/C'], {queryParams: params});
889 openBucketDialog(rows: HoldingsEntry[]) {
890 const copyIds = this.selectedCopyIds(rows);
891 if (copyIds.length > 0) {
892 this.bucketDialog.bucketClass = 'copy';
893 this.bucketDialog.itemIds = copyIds;
894 this.bucketDialog.open({size: 'lg'});
898 openConjoinedDialog(rows: HoldingsEntry[]) {
899 const copyIds = this.selectedCopyIds(rows);
900 if (copyIds.length > 0) {
901 this.conjoinedDialog.copyIds = copyIds;
902 this.conjoinedDialog.open({size: 'sm'});
906 bookItems(rows: HoldingsEntry[]) {
907 const copyIds = this.selectedCopyIds(rows);
908 if (copyIds.length > 0) {
909 this.router.navigate(
910 ['staff', 'booking', 'create_reservation', 'for_resource', rows.filter(r => Boolean(r.copy))[0].copy.barcode()]
915 makeBookable(rows: HoldingsEntry[]) {
916 const copyIds = this.selectedCopyIds(rows);
917 if (copyIds.length > 0) {
918 this.makeBookableDialog.copyIds = copyIds;
919 this.makeBookableDialog.open({});
923 manageReservations(rows: HoldingsEntry[]) {
924 const copyIds = this.selectedCopyIds(rows);
925 if (copyIds.length > 0) {
926 this.router.navigate(
927 ['staff', 'booking', 'manage_reservations', 'by_resource', rows.filter(r => Boolean(r.copy))[0].copy.barcode()]
932 transferSelectedItems(rows: HoldingsEntry[]) {
933 if (rows.length === 0) { return; }
936 this.localStore.getLocalItem('eg.cat.transfer_target_vol');
939 this.localStore.getLocalItem('eg.cat.transfer_target_lib');
942 this.localStore.getLocalItem('eg.cat.transfer_target_record');
946 if (cnId) { // Direct call number transfer
948 const itemIds = this.selectedCopyIds(rows);
949 promise = this.transferItems.transferItems(itemIds, cnId);
951 } else if (orgId && recId) { // "Auto" transfer
953 // Clone the items to be modified to avoid any unexpected
954 // modifications and fesh the call numbers.
955 const items = this.idl.clone(this.selectedCopies(rows));
956 items.forEach(i => i.call_number(
957 this.treeNodeCache.callNum[i.call_number()].target));
960 promise = this.transferItems.autoTransferItems(items, recId, orgId);
963 promise = this.transferAlert.open().toPromise();
966 promise.then(success => success ? this.hardRefresh() : null);
969 transferSelectedHoldings(rows: HoldingsEntry[]) {
970 const callNums = this.selectedCallNums(rows);
971 if (callNums.length === 0) { return; }
974 this.localStore.getLocalItem('eg.cat.transfer_target_lib');
977 this.localStore.getLocalItem('eg.cat.transfer_target_record');
980 // When transferring holdings (call numbers) between org units,
981 // limit transfers to within the current record.
982 recId = this.recordId;
985 // No destinations applied.
986 return this.transferAlert.open();
989 this.transferHoldings.targetRecId = recId;
990 this.transferHoldings.targetOrgId = orgId;
991 this.transferHoldings.callNums = callNums;
993 this.transferHoldings.transferHoldings()
994 .then(success => success ? this.hardRefresh() : null);