1 import {Component, OnInit, Input, ViewChild} from '@angular/core';
2 import {Observable, Observer, of} from 'rxjs';
3 import {map} from 'rxjs/operators';
4 import {Pager} from '@eg/share/util/pager';
5 import {IdlObject} from '@eg/core/idl.service';
6 import {NetService} from '@eg/core/net.service';
7 import {StaffCatalogService} from '../catalog.service';
8 import {OrgService} from '@eg/core/org.service';
9 import {AuthService} from '@eg/core/auth.service';
10 import {PcrudService} from '@eg/core/pcrud.service';
11 import {GridDataSource} from '@eg/share/grid/grid';
12 import {GridComponent} from '@eg/share/grid/grid.component';
13 import {GridToolbarCheckboxComponent} from '@eg/share/grid/grid-toolbar-checkbox.component';
14 import {ServerStoreService} from '@eg/core/server-store.service';
17 // The holdings grid models a single HoldingsTree, composed of HoldingsTreeNodes
18 // flattened on-demand into a list of HoldingEntry objects.
19 class HoldingsTreeNode {
20 children: HoldingsTreeNode[];
21 nodeType: 'org' | 'volume' | 'copy';
23 parentNode: HoldingsTreeNode;
33 root: HoldingsTreeNode;
35 this.root = new HoldingsTreeNode();
41 // org unit shortname, call number label, or copy barcode
42 locationLabel: string;
43 // location label indentation depth
44 locationDepth: number | null;
45 volumeCount: number | null;
46 copyCount: number | null;
47 callNumberLabel: string;
51 treeNode: HoldingsTreeNode;
55 selector: 'eg-holdings-maintenance',
56 templateUrl: 'holdings.component.html'
58 export class HoldingsMaintenanceComponent implements OnInit {
62 gridDataSource: GridDataSource;
63 gridTemplateContext: any;
64 @ViewChild('holdingsGrid') holdingsGrid: GridComponent;
66 // Manage visibility of various sub-sections
67 @ViewChild('volsCheckbox') volsCheckbox: GridToolbarCheckboxComponent;
68 @ViewChild('copiesCheckbox') copiesCheckbox: GridToolbarCheckboxComponent;
69 @ViewChild('emptyVolsCheckbox') emptyVolsCheckbox: GridToolbarCheckboxComponent;
70 @ViewChild('emptyLibsCheckbox') emptyLibsCheckbox: GridToolbarCheckboxComponent;
72 contextOrg: IdlObject;
73 holdingsTree: HoldingsTree;
74 holdingsTreeOrgCache: {[id: number]: HoldingsTreeNode};
75 refreshHoldings: boolean;
78 // List of copies whose due date we need to retrieve.
79 itemCircsNeeded: IdlObject[];
81 // When true draw the grid based on the stored preferences.
82 // When not true, render based on the current "expanded" state of each node.
83 // Rendering from prefs happens on initial load and when any prefs change.
84 renderFromPrefs: boolean;
85 rowClassCallback: (row: any) => string;
87 @Input() set recordId(id: number) {
89 // Only force new data collection when recordId()
90 // is invoked after ngInit() has already run.
92 this.refreshHoldings = true;
93 this.holdingsGrid.reload();
98 private net: NetService,
99 private org: OrgService,
100 private auth: AuthService,
101 private pcrud: PcrudService,
102 private staffCat: StaffCatalogService,
103 private store: ServerStoreService
105 // Set some sane defaults before settings are loaded.
106 this.contextOrg = this.org.get(this.auth.user().ws_ou());
107 this.gridDataSource = new GridDataSource();
108 this.refreshHoldings = true;
109 this.renderFromPrefs = true;
111 this.rowClassCallback = (row: any): string => {
112 if (row.volume && !row.copy) {
117 this.gridTemplateContext = {
118 toggleExpandRow: (row: HoldingsEntry) => {
119 row.treeNode.expanded = !row.treeNode.expanded;
121 if (!row.treeNode.expanded) {
122 // When collapsing a node, all child nodes should be
123 // collapsed as well.
124 const traverse = (node: HoldingsTreeNode) => {
125 node.expanded = false;
126 node.children.forEach(traverse);
128 traverse(row.treeNode);
131 this.holdingsGrid.reload();
134 copyIsHoldable: (copy: IdlObject): boolean => {
135 return copy.holdable() === 't'
136 && copy.location().holdable() === 't'
137 && copy.status().holdable() === 't';
143 this.initDone = true;
145 // These are pre-cached via the resolver.
146 const settings = this.store.getItemBatchCached([
147 'cat.holdings_show_empty_org',
148 'cat.holdings_show_empty',
149 'cat.holdings_show_copies',
150 'cat.holdings_show_vols'
153 this.volsCheckbox.checked(settings['cat.holdings_show_vols']);
154 this.copiesCheckbox.checked(settings['cat.holdings_show_copies']);
155 this.emptyVolsCheckbox.checked(settings['cat.holdings_show_empty']);
156 this.emptyLibsCheckbox.checked(settings['cat.holdings_show_empty_org']);
158 this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
159 return this.fetchHoldings(pager);
167 toggleShowCopies(value: boolean) {
168 this.store.setItem('cat.holdings_show_copies', value);
170 // Showing copies implies showing volumes
171 this.volsCheckbox.checked(true);
173 this.renderFromPrefs = true;
174 this.holdingsGrid.reload();
177 toggleShowVolumes(value: boolean) {
178 this.store.setItem('cat.holdings_show_vols', value);
180 // Hiding volumes implies hiding empty vols and copies.
181 this.copiesCheckbox.checked(false);
182 this.emptyVolsCheckbox.checked(false);
184 this.renderFromPrefs = true;
185 this.holdingsGrid.reload();
188 toggleShowEmptyVolumes(value: boolean) {
189 this.store.setItem('cat.holdings_show_empty', value);
191 this.volsCheckbox.checked(true);
193 this.renderFromPrefs = true;
194 this.holdingsGrid.reload();
197 toggleShowEmptyLibs(value: boolean) {
198 this.store.setItem('cat.holdings_show_empty_org', value);
199 this.renderFromPrefs = true;
200 this.holdingsGrid.reload();
203 onRowActivate(row: any) {
205 // Launch copy editor?
207 this.gridTemplateContext.toggleExpandRow(row);
213 // The initial tree simply matches the org unit tree
214 const traverseOrg = (node: HoldingsTreeNode) => {
215 node.expanded = true;
216 node.target.children().forEach((org: IdlObject) => {
217 const nodeChild = new HoldingsTreeNode();
218 nodeChild.nodeType = 'org';
219 nodeChild.target = org;
220 nodeChild.parentNode = node;
221 node.children.push(nodeChild);
222 this.holdingsTreeOrgCache[org.id()] = nodeChild;
223 traverseOrg(nodeChild);
227 this.holdingsTree = new HoldingsTree();
228 this.holdingsTree.root.nodeType = 'org';
229 this.holdingsTree.root.target = this.org.root();
231 this.holdingsTreeOrgCache = {};
232 this.holdingsTreeOrgCache[this.org.root().id()] = this.holdingsTree.root;
234 traverseOrg(this.holdingsTree.root);
237 // Org node children are sorted with any child org nodes pushed to the
238 // front, followed by the call number nodes sorted alphabetcially by label.
239 // TODO: prefix/suffix
240 sortOrgNodeChildren(node: HoldingsTreeNode) {
241 node.children = node.children.sort((a, b) => {
242 if (a.nodeType === 'org') {
243 if (b.nodeType === 'org') {
244 return a.target.shortname() < b.target.shortname() ? -1 : 1;
248 } else if (b.nodeType === 'org') {
251 return a.target.label() < b.target.label() ? -1 : 1;
256 // Sets call number and copy count sums to nodes that need it.
257 // Applies the initial expansed state of each container node.
258 setTreeCounts(node: HoldingsTreeNode) {
260 if (node.nodeType === 'org') {
262 node.volumeCount = 0;
263 } else if(node.nodeType === 'volume') {
267 let hasChildOrgWithData = false;
268 let hasChildOrgSansData = false;
269 node.children.forEach(child => {
270 this.setTreeCounts(child);
271 if (node.nodeType === 'org') {
272 node.copyCount += child.copyCount;
273 if (child.nodeType === 'volume') {
276 hasChildOrgWithData = child.volumeCount > 0;
277 hasChildOrgSansData = child.volumeCount === 0;
278 node.volumeCount += child.volumeCount;
280 } else if (node.nodeType === 'volume') {
281 node.copyCount = node.children.length;
282 if (this.renderFromPrefs) {
283 node.expanded = this.copiesCheckbox.checked();
288 if (this.renderFromPrefs && node.nodeType === 'org') {
289 if (node.copyCount > 0 && this.volsCheckbox.checked()) {
290 node.expanded = true;
291 } else if (node.volumeCount > 0 && this.emptyVolsCheckbox.checked()) {
292 node.expanded = true;
293 } else if (hasChildOrgWithData) {
294 node.expanded = true;
295 } else if (hasChildOrgSansData && this.emptyLibsCheckbox.checked()) {
296 node.expanded = true;
298 node.expanded = false;
303 // Create HoldingsEntry objects for tree nodes that should be displayed
304 // and relays them to the grid via the observer.
305 propagateTreeEntries(observer: Observer<HoldingsEntry>, node: HoldingsTreeNode) {
306 const entry = new HoldingsEntry();
307 entry.treeNode = node;
308 entry.index = this.gridIndex++;
310 switch(node.nodeType) {
312 if (this.renderFromPrefs && node.volumeCount === 0
313 && !this.emptyLibsCheckbox.checked()) {
316 entry.locationLabel = node.target.shortname();
317 entry.locationDepth = node.target.ou_type().depth();
318 entry.copyCount = node.copyCount;
319 entry.volumeCount = node.volumeCount;
320 this.sortOrgNodeChildren(node);
324 entry.locationLabel = node.target.label(); // TODO prefix/suffix
325 entry.locationDepth = node.parentNode.target.ou_type().depth() + 1;
326 entry.callNumberLabel = entry.locationLabel;
327 entry.volume = node.target;
328 entry.copyCount = node.copyCount;
332 entry.locationLabel = node.target.barcode();
333 entry.locationDepth = node.parentNode.parentNode.target.ou_type().depth() + 2;
334 entry.callNumberLabel = node.parentNode.target.label() // TODO
335 entry.volume = node.parentNode.target;
336 entry.copy = node.target;
337 entry.circ = node.target._circ;
341 // Tell the grid about the node entry
342 observer.next(entry);
345 // Process the child nodes.
346 node.children.forEach(child =>
347 this.propagateTreeEntries(observer, child));
351 // Turns the tree into a list of entries for grid display
352 flattenHoldingsTree(observer: Observer<HoldingsEntry>) {
354 this.setTreeCounts(this.holdingsTree.root);
355 this.propagateTreeEntries(observer, this.holdingsTree.root);
357 this.renderFromPrefs = false;
361 fetchHoldings(pager: Pager): Observable<any> {
362 if (!this.recId) { return of([]); }
364 return new Observable<any>(observer => {
366 if (!this.refreshHoldings) {
367 this.flattenHoldingsTree(observer);
371 this.initHoldingsTree();
372 this.itemCircsNeeded = [];
374 this.pcrud.search('acn',
375 { record: this.recId,
376 owning_lib: this.org.ancestors(this.contextOrg, true),
378 label: {'!=' : '##URI##'}
382 acp: ['status', 'location', 'circ_lib', 'parts',
383 'age_protect', 'copy_alerts', 'latest_inventory'],
384 acn: ['prefix', 'suffix', 'copies'],
385 acli: ['inventory_workstation']
389 vol => this.appendVolume(vol),
392 this.refreshHoldings = false;
393 this.fetchCircs().then(
394 ok => this.flattenHoldingsTree(observer)
401 // Retrieve circulation objects for checked out items.
402 fetchCircs(): Promise<any> {
403 const copyIds = this.itemCircsNeeded.map(copy => copy.id());
404 if (copyIds.length === 0) { return Promise.resolve(); }
406 return this.pcrud.search('circ', {
407 target_copy: copyIds,
409 }).pipe(map(circ => {
410 const copy = this.itemCircsNeeded.filter(
411 c => Number(c.id()) === Number(circ.target_copy()))[0];
416 appendVolume(volume: IdlObject) {
418 const volNode = new HoldingsTreeNode();
419 volNode.parentNode = this.holdingsTreeOrgCache[volume.owning_lib()];
420 volNode.parentNode.children.push(volNode);
421 volNode.nodeType = 'volume';
422 volNode.target = volume;
425 .sort((a: IdlObject, b: IdlObject) => a.barcode() < b.barcode() ? -1 : 1)
426 .forEach((copy: IdlObject) => {
427 const copyNode = new HoldingsTreeNode();
428 copyNode.parentNode = volNode;
429 volNode.children.push(copyNode);
430 copyNode.nodeType = 'copy';
431 copyNode.target = copy;
432 const stat = Number(copy.status().id());
433 if (stat === 1 /* checked out */ || stat === 16 /* long overdue */) {
434 this.itemCircsNeeded.push(copy);