Support for various context menu actions.
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Dan Wells <dbw2@calvin.edu>
// Useful for massaging the match string prior to comparison
// and display. Default version trims leading/trailing spaces.
- formatDisplayString: (ComboboxEntry) => string;
+ formatDisplayString: (e: ComboboxEntry) => string;
constructor(
private elm: ElementRef,
StaffCommonModule,
CatalogCommonModule,
CatalogRoutingModule,
- HoldsModule
+ HoldsModule,
+ HoldingsModule
],
providers: [
StaffCatalogService
--- /dev/null
+/*
+:host /deep/ allows us to share style with child components.
+In this case, the holdings components wants its grid to see
+the CSS we have defined for different row types
+
+See https://v2.angular.io/docs/ts/latest/guide/component-styles.html
+*/
+
+/* grid row colors are bootstrap class="bg-info" with opacity */
+
+/*
+:host /deep/ .holdings-copy-row {
+}
+*/
+
+:host /deep/ .holdings-volume-row {
+ color: #004085;
+ background-color: rgb(23,162,184,0.2);
+}
+
+:host /deep/ .holdings-org-row-0 {
+ color: #004085;
+ background-color: rgb(23,162,184);
+}
+
+:host /deep/ .holdings-org-row-1 {
+ color: #004085;
+ background-color: rgb(23,162,184,0.8);
+}
+
+:host /deep/ .holdings-org-row-2 {
+ color: #004085;
+ background-color: rgb(23,162,184,0.6);
+}
+
+:host /deep/ .holdings-org-row-3 {
+ color: #004085;
+ background-color: rgb(23,162,184,0.4);
+}
+
+/* Add additional classes for more deeply nested org unit hierarchies */
\ No newline at end of file
+<!-- org unit selector -->
+
+<div class="row mt-3">
+ <div class="col-lg-4">
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <div class="input-group-text" i18n>Holdings Maintenance</div>
+ </div>
+ <eg-org-select [initialOrg]="contextOrg"
+ (onChange)="contextOrgChanged($event)">
+ </eg-org-select>
+ </div>
+ </div>
+</div>
+
+<!-- Location / Barcode cell template -->
+
<ng-template #locationTemplate let-row="row" let-userContext="userContext">
<!-- pl-* is doubled for added impact -->
<div class="pl-{{row.locationDepth}}">
</div>
</ng-template>
+<!-- Holdable true/false display -->
+
<ng-template #holdableTemplate let-row="row" let-userContext="userContext">
<ng-container *ngIf="row.copy">
- <ng-container *ngIf="userContext.copyIsHoldable(row.copy); else notHoldable">
- <span i18n>Yes</span>
- </ng-container>
- <ng-template #notHoldable><span i18n>No</span></ng-template>
+ <eg-bool [value]="userContext.copyIsHoldable(row.copy)">
+ </eg-bool>
</ng-container>
</ng-template>
+<eg-mark-damaged-dialog #markDamagedDialog></eg-mark-damaged-dialog>
+<eg-mark-missing-dialog #markMissingDialog></eg-mark-missing-dialog>
+<eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
+<eg-replace-barcode-dialog #replaceBarcode></eg-replace-barcode-dialog>
+<!-- holdings grid -->
<div class='eg-copies w-100 mt-3'>
<eg-grid #holdingsGrid [dataSource]="gridDataSource"
- (onRowActivate)="onRowActivate($event)"
- [pageSize]="50" [rowClassCallback]="rowClassCallback"
+ (onRowActivate)="onRowActivate($event)" [disablePaging]="true"
+ [rowClassCallback]="rowClassCallback"
[sortable]="false" persistKey="cat.holdings">
- <!-- checkboxes -->
+ <!-- checkboxes / filters -->
<eg-grid-toolbar-checkbox i18n-label label="Show Volumes"
#volsCheckbox (onChange)="toggleShowVolumes($event)">
#emptyLibsCheckbox (onChange)="toggleShowEmptyLibs($event)">
</eg-grid-toolbar-checkbox>
+ <!-- row actions -->
+
+ <!-- row actions : Ungrouped -->
+
+ <eg-grid-toolbar-action
+ i18n-label label="Print Labels" (onClick)="openItemPrintLabels($event)">
+ </eg-grid-toolbar-action>
+
+ <!-- row actions : Add -->
+
+ <eg-grid-toolbar-action
+ i18n-group group="Add" i18n-label label="Add Call Numbers"
+ (onClick)="openVolCopyEdit($event, true, false)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Add" i18n-label label="Add Items"
+ (onClick)="openVolCopyEdit($event, false, true)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Add" i18n-label label="Add Call Numbers and Items"
+ (onClick)="openVolCopyEdit($event, true, true)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Add" i18n-label label="Add Item Alerts"
+ (onClick)="openItemNotes($event, 'create')">
+ </eg-grid-toolbar-action>
+
+ <!-- row actions: Edit -->
+
+ <eg-grid-toolbar-action
+ i18n-group group="Edit" i18n-label label="Edit Call Numbers"
+ (onClick)="openVolCopyEdit($event, true, false)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Edit" i18n-label label="Edit Call Numbers And Items"
+ (onClick)="openVolCopyEdit($event, true, true)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Edit" i18n-label label="Edit Items"
+ (onClick)="openVolCopyEdit($event, false, true)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Edit" i18n-label label="Edit Item Alerts"
+ (onClick)="openItemNotes($event, 'manage')">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Edit" i18n-label label="Replace Barcodes"
+ (onClick)="openReplaceBarcodeDialog($event)">
+ </eg-grid-toolbar-action>
+
+ <!-- row actions : Show -->
+
+ <eg-grid-toolbar-action
+ i18n-group group="Show" i18n-label label="Show Item Status (list)"
+ (onClick)="openItemStatusList($event)"></eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Show" i18n-label label="Show Item Status (detail)"
+ (onClick)="openItemStatus($event)"></eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Show" i18n-label label="Show Item Holds"
+ (onClick)="openItemHolds($event)"></eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Show" i18n-label label="Show Triggered Events"
+ (onClick)="openItemTriggeredEvents($event)"></eg-grid-toolbar-action>
+
+ <!-- row actions : Mark -->
+
+ <eg-grid-toolbar-action
+ group="Mark" i18n-group i18n-label label="Mark Item Damaged"
+ (onClick)="showMarkDamagedDialog($event)"></eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Mark" i18n-label label="Mark Item Missing"
+ (onClick)="showMarkMissingDialog($event)">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action
+ i18n-group group="Mark"
+ i18n-label label="Mark Library/Call Number as Transfer Destination"
+ (onClick)="markLibCnForTransfer($event)">
+ </eg-grid-toolbar-action>
+
+
<!-- fields -->
+ <!-- NOTE column names were added to match the names from the AngJS grid
+ so grid settings would propagate -->
+
<eg-grid-column path="index" [hidden]="true" [index]="true">
</eg-grid-column>
- <eg-grid-column path="copy.id" [hidden]="true" label="Copy ID" i18n-label>
+ <eg-grid-column name="id" path="copy.id" [hidden]="true" label="Copy ID" i18n-label>
</eg-grid-column>
<eg-grid-column path="volume.id" [hidden]="true" label="Volume ID" i18n-label>
</eg-grid-column>
- <eg-grid-column name="location_barcode" [flex]="4"
+ <eg-grid-column name="owner_label" [flex]="4"
[cellTemplate]="locationTemplate" [cellContext]="gridTemplateContext"
label="Location/Barcode" [disableTooltip]="true" i18n-label>
</eg-grid-column>
</eg-grid-column>
<eg-grid-column path="copyCount" datatype="number" label="Copies" i18n-label>
</eg-grid-column>
- <eg-grid-column path="callNumberLabel" label="Call Number" i18n-label>
+ <eg-grid-column path="volume._label" name="call_number.label" label="Call Number" i18n-label>
+ </eg-grid-column>
+ <eg-grid-column path="copy.barcode" name="barcode" label="Barcode" i18n-label>
</eg-grid-column>
<eg-grid-column i18n-label label="Circ Library" path="copy.circ_lib"
- datatype="org_unit"></eg-grid-column>
+ name="circ_lib.name" datatype="org_unit"></eg-grid-column>
<eg-grid-column i18n-label label="Owning Library" path="volume.owning_lib"
datatype="org_unit"></eg-grid-column>
<eg-grid-column i18n-label label="Due Date" path="circ.due_date"
datatype="timestamp"></eg-grid-column>
- <eg-grid-column i18n-label label="Shelving Location" path="copy.location.name">
+ <eg-grid-column i18n-label label="Shelving Location" path="copy.location.name" name="location.name">
</eg-grid-column>
- <eg-grid-column i18n-label label="Circulation Modifier" path="copy.circ_modifier">
+ <eg-grid-column i18n-label label="Circulation Modifier"
+ path="copy.circ_modifier" name="circ_modifier">
+ </eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Number" path="copy.copy_number" name="copy_number" [hidden]="true">
</eg-grid-column>
- <eg-grid-column i18n-label label="Status" path="copy.status.name">
+ <eg-grid-column i18n-label label="Status" path="copy.status.name" name="status_name">
+ </eg-grid-column>
+ <eg-grid-column i18n-label label="Call Number Prefix"
+ path="volume.prefix.label" name="call_number.prefix.label" [hidden]="true">
+ </eg-grid-column>
+ <eg-grid-column i18n-label label="Call Number Suffix"
+ path="volume.suffix.label" name="call_number.suffix.label" [hidden]="true">
</eg-grid-column>
<eg-grid-column i18n-label label="Active/Create Date"
path="copy.active_date" datatype="timestamp">
</eg-grid-column>
<eg-grid-column i18n-label label="Age Hold Protection"
- path="copy.age_protect.name"></eg-grid-column>
+ path="copy.age_protect.name" name="age_protect.name"></eg-grid-column>
+ <eg-grid-column i18n-label label="Copy Price"
+ path="copy.price" name="price" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Circulate" path="copy.circulate"
+ name="circulate" datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Deposit" path="copy.deposit"
+ name="deposit" datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Deposit Amount" path="copy.deposit_amount"
+ name="deposit_amount" datatype="money" [hidden]="true"></eg-grid-column>
<eg-grid-column i18n-label label="Holdable?" name="holdable"
[cellTemplate]="holdableTemplate" [cellContext]="gridTemplateContext">
</eg-grid-column>
-
+ <eg-grid-column i18n-label label="Reference?" path="copy.ref"
+ name="ref" datatype="bool" [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Last Inventory Date"
+ path="copy.latest_inventory.inventory_date"
+ name="latest_inventory.inventory_date" datatype="timestamp" [hidden]="true">
+ </eg-grid-column>
+ <eg-grid-column i18n-label label="Last Inventory Workstation"
+ path="copy.latest_inventory.inventory_workstation.name"
+ name="latest_inventory.inventory_workstation.name" [hidden]="true">
+ </eg-grid-column>
</eg-grid>
</div>
import {map} from 'rxjs/operators';
import {Pager} from '@eg/share/util/pager';
import {IdlObject} from '@eg/core/idl.service';
-import {NetService} from '@eg/core/net.service';
import {StaffCatalogService} from '../catalog.service';
import {OrgService} from '@eg/core/org.service';
-import {AuthService} from '@eg/core/auth.service';
import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
import {GridDataSource} from '@eg/share/grid/grid';
import {GridComponent} from '@eg/share/grid/grid.component';
-import {GridToolbarCheckboxComponent} from '@eg/share/grid/grid-toolbar-checkbox.component';
+import {GridToolbarCheckboxComponent
+ } from '@eg/share/grid/grid-toolbar-checkbox.component';
+import {StoreService} from '@eg/core/store.service';
import {ServerStoreService} from '@eg/core/server-store.service';
-
+import {MarkDamagedDialogComponent
+ } from '@eg/staff/share/holdings/mark-damaged-dialog.component';
+import {MarkMissingDialogComponent
+ } from '@eg/staff/share/holdings/mark-missing-dialog.component';
+import {AnonCacheService} from '@eg/share/util/anon-cache.service';
+import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
+import {CopyAlertsDialogComponent
+ } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {ReplaceBarcodeDialogComponent
+ } from '@eg/staff/share/holdings/replace-barcode-dialog.component';
// The holdings grid models a single HoldingsTree, composed of HoldingsTreeNodes
// flattened on-demand into a list of HoldingEntry objects.
@Component({
selector: 'eg-holdings-maintenance',
- templateUrl: 'holdings.component.html'
+ templateUrl: 'holdings.component.html',
+ styleUrls: ['holdings.component.css']
})
export class HoldingsMaintenanceComponent implements OnInit {
- recId: number;
initDone = false;
gridDataSource: GridDataSource;
gridTemplateContext: any;
@ViewChild('holdingsGrid') holdingsGrid: GridComponent;
// Manage visibility of various sub-sections
- @ViewChild('volsCheckbox') volsCheckbox: GridToolbarCheckboxComponent;
- @ViewChild('copiesCheckbox') copiesCheckbox: GridToolbarCheckboxComponent;
- @ViewChild('emptyVolsCheckbox') emptyVolsCheckbox: GridToolbarCheckboxComponent;
- @ViewChild('emptyLibsCheckbox') emptyLibsCheckbox: GridToolbarCheckboxComponent;
+ @ViewChild('volsCheckbox')
+ private volsCheckbox: GridToolbarCheckboxComponent;
+ @ViewChild('copiesCheckbox')
+ private copiesCheckbox: GridToolbarCheckboxComponent;
+ @ViewChild('emptyVolsCheckbox')
+ private emptyVolsCheckbox: GridToolbarCheckboxComponent;
+ @ViewChild('emptyLibsCheckbox')
+ private emptyLibsCheckbox: GridToolbarCheckboxComponent;
+ @ViewChild('markDamagedDialog')
+ private markDamagedDialog: MarkDamagedDialogComponent;
+ @ViewChild('markMissingDialog')
+ private markMissingDialog: MarkMissingDialogComponent;
+ @ViewChild('copyAlertsDialog')
+ private copyAlertsDialog: CopyAlertsDialogComponent;
+ @ViewChild('replaceBarcode')
+ private replaceBarcode: ReplaceBarcodeDialogComponent;
- contextOrg: IdlObject;
holdingsTree: HoldingsTree;
- holdingsTreeOrgCache: {[id: number]: HoldingsTreeNode};
+
+ // nodeType => id => tree node cache
+ treeNodeCache: {[nodeType: string]: {[id: number]: HoldingsTreeNode}};
+
+ // When true and a grid reload is called, the holdings data will be
+ // re-fetched from the server.
refreshHoldings: boolean;
+
+ // Used as a row identifier in th grid, since we're mixing object types.
gridIndex: number;
// List of copies whose due date we need to retrieve.
// When not true, render based on the current "expanded" state of each node.
// Rendering from prefs happens on initial load and when any prefs change.
renderFromPrefs: boolean;
+
rowClassCallback: (row: any) => string;
+ private _recId: number;
@Input() set recordId(id: number) {
- this.recId = id;
+ this._recId = id;
// Only force new data collection when recordId()
// is invoked after ngInit() has already run.
if (this.initDone) {
- this.refreshHoldings = true;
- this.holdingsGrid.reload();
+ this.hardRefresh();
}
}
+ get recordId(): number {
+ return this._recId;
+ }
+
+ contextOrg: IdlObject;
constructor(
- private net: NetService,
private org: OrgService,
- private auth: AuthService,
private pcrud: PcrudService,
+ private auth: AuthService,
private staffCat: StaffCatalogService,
- private store: ServerStoreService
+ private store: ServerStoreService,
+ private localStore: StoreService,
+ private holdings: HoldingsService,
+ private anonCache: AnonCacheService
) {
// Set some sane defaults before settings are loaded.
- this.contextOrg = this.org.get(this.auth.user().ws_ou());
this.gridDataSource = new GridDataSource();
this.refreshHoldings = true;
this.renderFromPrefs = true;
+ // TODO: need a separate setting for this?
+ this.contextOrg = this.staffCat.searchContext.searchOrg;
+
this.rowClassCallback = (row: any): string => {
- if (row.volume && !row.copy) {
- return 'bg-info';
+ if (row.volume) {
+ if (row.copy) {
+ return 'holdings-copy-row';
+ } else {
+ return 'holdings-volume-row';
+ }
+ } else {
+ // Add a generic org unit class and a depth-specific
+ // class for styling different levels of the org tree.
+ return 'holdings-org-row holdings-org-row-' +
+ row.treeNode.target.ou_type().depth();
}
}
ngOnInit() {
this.initDone = true;
- // These are pre-cached via the resolver.
+ // These are pre-cached via the catalog resolver.
const settings = this.store.getItemBatchCached([
'cat.holdings_show_empty_org',
'cat.holdings_show_empty',
'cat.holdings_show_vols'
]);
- this.volsCheckbox.checked(settings['cat.holdings_show_vols']);
+ // Show volumes by default when no preference is set.
+ let showVols = settings['cat.holdings_show_vols'];
+ if (showVols === null) { showVols = true; }
+
+ this.volsCheckbox.checked(showVols);
this.copiesCheckbox.checked(settings['cat.holdings_show_copies']);
this.emptyVolsCheckbox.checked(settings['cat.holdings_show_empty']);
this.emptyLibsCheckbox.checked(settings['cat.holdings_show_empty_org']);
+ this.initHoldingsTree();
this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
return this.fetchHoldings(pager);
};
}
- ngAfterViewInit() {
+ contextOrgChanged(org: IdlObject) {
+ this.contextOrg = org;
+ this.hardRefresh();
+ }
+ hardRefresh() {
+ this.renderFromPrefs = true;
+ this.refreshHoldings = true;
+ this.initHoldingsTree();
+ this.holdingsGrid.reload();
}
toggleShowCopies(value: boolean) {
initHoldingsTree() {
+ const visibleOrgs = this.org.fullPath(this.contextOrg, true);
+
// The initial tree simply matches the org unit tree
const traverseOrg = (node: HoldingsTreeNode) => {
- node.expanded = true;
node.target.children().forEach((org: IdlObject) => {
+ if (visibleOrgs.indexOf(org.id()) == -1) {
+ return; // Org is outside of scope
+ }
const nodeChild = new HoldingsTreeNode();
nodeChild.nodeType = 'org';
nodeChild.target = org;
nodeChild.parentNode = node;
node.children.push(nodeChild);
- this.holdingsTreeOrgCache[org.id()] = nodeChild;
+ this.treeNodeCache.org[org.id()] = nodeChild;
traverseOrg(nodeChild);
});
}
+ this.treeNodeCache = {
+ org: {},
+ volume: {},
+ copy: {}
+ };
+
this.holdingsTree = new HoldingsTree();
this.holdingsTree.root.nodeType = 'org';
this.holdingsTree.root.target = this.org.root();
-
- this.holdingsTreeOrgCache = {};
- this.holdingsTreeOrgCache[this.org.root().id()] = this.holdingsTree.root;
+ this.treeNodeCache.org[this.org.root().id()] = this.holdingsTree.root;
traverseOrg(this.holdingsTree.root);
}
// Org node children are sorted with any child org nodes pushed to the
// front, followed by the call number nodes sorted alphabetcially by label.
- // TODO: prefix/suffix
sortOrgNodeChildren(node: HoldingsTreeNode) {
node.children = node.children.sort((a, b) => {
if (a.nodeType === 'org') {
} else if (b.nodeType === 'org') {
return 1;
} else {
- return a.target.label() < b.target.label() ? -1 : 1;
+ // TODO: should this use label sortkey instead of
+ // the compiled volume label?
+ return a.target._label < b.target._label ? -1 : 1;
}
});
}
switch(node.nodeType) {
case 'org':
- if (this.renderFromPrefs && node.volumeCount === 0
+ if (node.volumeCount === 0
&& !this.emptyLibsCheckbox.checked()) {
return;
}
break;
case 'volume':
- entry.locationLabel = node.target.label(); // TODO prefix/suffix
+ if (this.renderFromPrefs) {
+ if (!this.volsCheckbox.checked()) {
+ return;
+ }
+ if (node.copyCount === 0
+ && !this.emptyVolsCheckbox.checked()) {
+ return;
+ }
+ }
+ entry.locationLabel = node.target._label;
entry.locationDepth = node.parentNode.target.ou_type().depth() + 1;
entry.callNumberLabel = entry.locationLabel;
entry.volume = node.target;
this.renderFromPrefs = false;
}
-
+ // Grab volumes, copies, and related data.
fetchHoldings(pager: Pager): Observable<any> {
- if (!this.recId) { return of([]); }
+ if (!this.recordId) { return of([]); }
return new Observable<any>(observer => {
return;
}
- this.initHoldingsTree();
this.itemCircsNeeded = [];
this.pcrud.search('acn',
- { record: this.recId,
- owning_lib: this.org.ancestors(this.contextOrg, true),
+ { record: this.recordId,
+ owning_lib: this.org.fullPath(this.contextOrg, true),
deleted: 'f',
label: {'!=' : '##URI##'}
}, {
acn: ['prefix', 'suffix', 'copies'],
acli: ['inventory_workstation']
}
- }
+ },
+ {authoritative: true}
).subscribe(
vol => this.appendVolume(vol),
err => {},
})).toPromise();
}
+ // Compile prefix + label + suffix into field volume._label;
+ setVolumeLabel(volume: IdlObject) {
+ const pfx = volume.prefix() ? volume.prefix().label() : '';
+ const sfx = volume.suffix() ? volume.suffix().label() : '';
+ volume._label = pfx ? pfx + ' ' : '';
+ volume._label += volume.label();
+ volume._label += sfx ? ' ' + sfx : '';
+ }
+
+ // Create the tree node for the volume if it doesn't already exist.
+ // Do the same for its linked copies.
appendVolume(volume: IdlObject) {
+ let volNode = this.treeNodeCache.volume[volume.id()];
+ this.setVolumeLabel(volume);
+
+ if (volNode) {
+ const pNode = this.treeNodeCache.org[volume.owning_lib()]
+ if (volNode.parentNode.target.id() !== pNode.target.id()) {
+ // Volume owning library changed. Un-link it from the previous
+ // org unit collection before adding to the new one.
+ // XXX TODO: ^--
+ volNode.parentNode = pNode;
+ volNode.parentNode.children.push(volNode);
+ }
+ } else {
+ volNode = new HoldingsTreeNode();
+ volNode.nodeType = 'volume';
+ volNode.parentNode = this.treeNodeCache.org[volume.owning_lib()]
+ volNode.parentNode.children.push(volNode);
+ this.treeNodeCache.volume[volume.id()] = volNode;
+ }
- const volNode = new HoldingsTreeNode();
- volNode.parentNode = this.holdingsTreeOrgCache[volume.owning_lib()];
- volNode.parentNode.children.push(volNode);
- volNode.nodeType = 'volume';
volNode.target = volume;
volume.copies()
.sort((a: IdlObject, b: IdlObject) => a.barcode() < b.barcode() ? -1 : 1)
- .forEach((copy: IdlObject) => {
- const copyNode = new HoldingsTreeNode();
+ .forEach((copy: IdlObject) => this.appendCopy(volNode, copy));
+ }
+
+ // Find or create a copy node.
+ appendCopy(volNode: HoldingsTreeNode, copy: IdlObject) {
+ let copyNode = this.treeNodeCache.copy[copy.id()];
+
+ if (copyNode) {
+ const oldParent = copyNode.parentNode;
+ if (oldParent.target.id() !== volNode.target.id()) {
+ // TODO: copy changed owning volume. Remove it from
+ // the previous volume before adding to the new volume.
copyNode.parentNode = volNode;
volNode.children.push(copyNode);
- copyNode.nodeType = 'copy';
- copyNode.target = copy;
- const stat = Number(copy.status().id());
- if (stat === 1 /* checked out */ || stat === 16 /* long overdue */) {
- this.itemCircsNeeded.push(copy);
- }
- });
+ }
+ } else {
+ // New node required
+ copyNode = new HoldingsTreeNode();
+ copyNode.nodeType = 'copy';
+ volNode.children.push(copyNode);
+ copyNode.parentNode = volNode;
+ this.treeNodeCache.copy[copy.id()] = copyNode;
+ }
+
+ copyNode.target = copy;
+ const stat = Number(copy.status().id());
+
+ if (stat === 1 /* checked out */ || stat === 16 /* long overdue */) {
+ // Avoid looking up circs on items that are not checked out.
+ this.itemCircsNeeded.push(copy);
+ }
}
-}
+ // Which copies in the grid are selected.
+ selectedCopyIds(rows: HoldingsEntry[], skipStatus?: number): number[] {
+ 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()));
+ }
+
+ selectedVolumeIds(rows: HoldingsEntry[]): number[] {
+ return rows
+ .filter(r => r.treeNode.nodeType === 'volume')
+ .map(r => Number(r.volume.id()));
+ }
+
+ async showMarkDamagedDialog(rows: HoldingsEntry[]) {
+ const copyIds = this.selectedCopyIds(rows, 14 /* ignore damaged */);
+
+ if (copyIds.length === 0) { return; }
+
+ let rowsModified = false;
+
+ const markNext = async(ids: number[]) => {
+ if (ids.length === 0) {
+ return Promise.resolve();
+ }
+
+ this.markDamagedDialog.copyId = ids.pop();
+ return this.markDamagedDialog.open({size: 'lg'}).then(
+ ok => {
+ if (ok) { rowsModified = true; }
+ return markNext(ids);
+ },
+ dismiss => markNext(ids)
+ );
+ };
+
+ await markNext(copyIds);
+ if (rowsModified) {
+ this.refreshHoldings = true;
+ this.holdingsGrid.reload();
+ }
+ }
+
+ showMarkMissingDialog(rows: any[]) {
+ const copyIds = this.selectedCopyIds(rows, 4 /* ignore missing */);
+ if (copyIds.length > 0) {
+ this.markMissingDialog.copyIds = copyIds;
+ this.markMissingDialog.open({}).then(
+ rowsModified => {
+ if (rowsModified) {
+ this.refreshHoldings = true;
+ this.holdingsGrid.reload();
+ }
+ },
+ dismissed => {} // avoid console errors
+ );
+ }
+ }
+
+ // Mark record, library, and potentially the selected call number
+ // as the current transfer target.
+ markLibCnForTransfer(rows: HoldingsEntry[]) {
+ if (rows.length === 0) {
+ return;
+ }
+
+ // Action may only apply to a single org or volume row.
+ const node = rows[0].treeNode;
+ if (node.nodeType === 'copy') {
+ return;
+ }
+
+ let orgId: number;
+
+ if (node.nodeType === 'org') {
+ orgId = node.target.id();
+
+ // Clear volume target when performed on an org unit row
+ this.localStore.removeLocalItem('eg.cat.transfer_target_vol');
+ } else if (node.nodeType === 'volume') {
+
+ // All volume nodes are children of org nodes.
+ orgId = node.parentNode.target.id();
+
+ // Add volume target when performed on a volume row.
+ this.localStore.setLocalItem(
+ 'eg.cat.transfer_target_vol', node.target.id())
+ }
+
+ this.localStore.setLocalItem('eg.cat.transfer_target_record', this.recordId);
+ this.localStore.setLocalItem('eg.cat.transfer_target_lib', orgId);
+ }
+
+ openAngJsWindow(path: string) {
+ const url = `/eg/staff/${path}`;
+ window.open(url, '_blank');
+ }
+
+ openItemHolds(rows: HoldingsEntry[]) {
+ if (rows.length > 0 && rows[0].copy) {
+ this.openAngJsWindow(`cat/item/${rows[0].copy.id()}/holds`);
+ }
+ }
+
+ openItemStatusList(rows: HoldingsEntry[]) {
+ const ids = this.selectedCopyIds(rows);
+ if (ids.length > 0) {
+ return this.openAngJsWindow(`cat/item/search/${ids.join(',')}`);
+ }
+ }
+
+ openItemStatus(rows: HoldingsEntry[]) {
+ if (rows.length > 0 && rows[0].copy) {
+ return this.openAngJsWindow(`cat/item/${rows[0].copy.id()}`);
+ }
+ }
+
+ openItemTriggeredEvents(rows: HoldingsEntry[]) {
+ if (rows.length > 0 && rows[0].copy) {
+ return this.openAngJsWindow(
+ `cat/item/${rows[0].copy.id()}/triggered_events`);
+ }
+ }
+
+ openItemPrintLabels(rows: HoldingsEntry[]) {
+ const ids = this.selectedCopyIds(rows);
+ if (ids.length === 0) { return; }
+
+ this.anonCache.setItem(null, 'print-labels-these-copies', {copies: ids})
+ .then(key => this.openAngJsWindow(`cat/printlabels/${key}`));
+ }
+
+ openVolCopyEdit(rows: HoldingsEntry[], addVols: boolean, addCopies: boolean) {
+
+ // The user may select a set of volumes by selecting volume and/or
+ // copy rows.
+ const volumes = [];
+ rows.forEach(r => {
+ if (r.treeNode.nodeType === 'volume') {
+ volumes.push(r.volume);
+ } else if (r.treeNode.nodeType === 'copy') {
+ volumes.push(r.treeNode.parentNode.target);
+ }
+ });
+
+ if (addCopies && !addVols) {
+ // Adding copies to an existing set of volumes.
+ if (volumes.length > 0) {
+ const volIds = volumes.map(v => Number(v.id()));
+ this.holdings.spawnAddHoldingsUi(this.recordId, volIds);
+ }
+
+ } else if (addVols) {
+ const entries = [];
+
+ if (volumes.length > 0) {
+
+ // When adding volumes, if any are selected in the grid,
+ // create volumes that have the same label and owner.
+ volumes.forEach(v =>
+ entries.push({label: v.label(), owner: v.owning_lib()}));
+
+ } else {
+
+ // Otherwise create new volumes from scratch.
+ entries.push({owner: this.auth.user().ws_ou()})
+ }
+
+ this.holdings.spawnAddHoldingsUi(
+ this.recordId, null, entries, !addCopies);
+ }
+ }
+
+ openItemNotes(rows: HoldingsEntry[], mode: string) {
+ const copyIds = this.selectedCopyIds(rows);
+ if (copyIds.length === 0) { return; }
+
+ this.copyAlertsDialog.copyIds = copyIds;
+ this.copyAlertsDialog.mode = mode;
+ this.copyAlertsDialog.open({size: 'lg'}).then(
+ modified => {
+ if (modified) {
+ this.hardRefresh();
+ }
+ },
+ dismissed => {}
+ )
+ }
+
+ openReplaceBarcodeDialog(rows: HoldingsEntry[]) {
+ const ids = this.selectedCopyIds(rows);
+ if (ids.length === 0) { return; }
+ this.replaceBarcode.copyIds = ids;
+ this.replaceBarcode.open({}).then(
+ modified => {
+ if (modified) {
+ this.hardRefresh();
+ }
+ },
+ dismissed => {}
+ );
+ }
+}
initDone = false;
searchContext: CatalogSearchContext;
+ _recordTab: string;
+ @Input() set recordTab(tab: string) {
+ this._recordTab = tab;
+ }
+ get recordTab(): string {
+ return this._recordTab;
+ }
+
@Input() set recordId(id: number) {
this.id = id;
// Only apply new record data after the initial load
this.setIndex();
}
+ routeToRecord(id: number) {
+ let url = '/staff/catalog/record/' + id;
+ if (this.recordTab) { url += '/' + this.recordTab; }
+ const params = this.catUrl.toUrlParams(this.searchContext);
+ this.router.navigate([url], {queryParams: params});
+ }
+
firstRecord(): void {
- this.findRecordAtIndex(0).then(id => {
- const params = this.catUrl.toUrlParams(this.searchContext);
- this.router.navigate(
- ['/staff/catalog/record/' + id], {queryParams: params});
- });
+ this.findRecordAtIndex(0)
+ .then(id => this.routeToRecord(id));
}
lastRecord(): void {
- this.findRecordAtIndex(
- this.searchContext.result.count - 1
- ).then(id => {
- const params = this.catUrl.toUrlParams(this.searchContext);
- this.router.navigate(
- ['/staff/catalog/record/' + id], {queryParams: params});
- });
+ this.findRecordAtIndex(this.searchContext.result.count - 1)
+ .then(id => this.routeToRecord(id));
}
nextRecord(): void {
- this.findRecordAtIndex(this.index + 1).then(id => {
- const params = this.catUrl.toUrlParams(this.searchContext);
- this.router.navigate(
- ['/staff/catalog/record/' + id], {queryParams: params});
- });
+ this.findRecordAtIndex(this.index + 1)
+ .then(id => this.routeToRecord(id));
}
prevRecord(): void {
- this.findRecordAtIndex(this.index - 1).then(id => {
- const params = this.catUrl.toUrlParams(this.searchContext);
- this.router.navigate(
- ['/staff/catalog/record/' + id], {queryParams: params});
- });
+ this.findRecordAtIndex(this.index - 1)
+ .then(id => this.routeToRecord(id));
}
-
// Returns the offset of the record within the search results as a whole.
searchIndex(idx: number): number {
return idx + this.searchContext.pager.offset;
<div class="row ml-0 mr-0">
<div id='staff-catalog-bib-navigation'>
<div *ngIf="searchContext.isSearchable()">
- <eg-catalog-record-pagination [recordId]="recordId">
+ <eg-catalog-record-pagination [recordId]="recordId" [recordTab]="recordTab">
</eg-catalog-record-pagination>
</div>
</div>
'cat.holdings_show_empty',
'cat.marcedit.stack_subfields',
'cat.marcedit.flateditor',
- 'eg.cat.record.summary.collapse',
'cat.holdings_show_copies',
'cat.holdings_show_vols'
]).then(settings => {
<div>
<a class="with-material-icon no-href text-primary"
title="Show More" i18n-title
- *ngIf="!expandDisplay" (click)="expandDisplay=true">
+ *ngIf="!expand" (click)="expand=true">
<span class="material-icons">expand_more</span>
</a>
<a class="with-material-icon no-href text-primary"
title="Show Less" i18n-title
- *ngIf="expandDisplay" (click)="expandDisplay=false">
+ *ngIf="expand" (click)="expand=false">
<span class="material-icons">expand_less</span>
</a>
</div>
</div>
</div>
</li>
- <li class="list-group-item" *ngIf="expandDisplay">
+ <li class="list-group-item" *ngIf="expand">
<div class="d-flex">
<div class="flex-1 font-weight-bold" i18n>Author:</div>
<div class="flex-3">{{summary.display.author}}</div>
</div>
</div>
</li>
- <li class="list-group-item" *ngIf="expandDisplay">
+ <li class="list-group-item" *ngIf="expand">
<div class="d-flex">
<div class="flex-1 font-weight-bold" i18n>Bib Call #:</div>
<div class="flex-3">{{summary.bibCallNumber}}</div>
import {Component, OnInit, Input} from '@angular/core';
-import {NetService} from '@eg/core/net.service';
import {OrgService} from '@eg/core/org.service';
-import {PcrudService} from '@eg/core/pcrud.service';
-import {CatalogService} from '@eg/share/catalog/catalog.service';
-import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+import {BibRecordService, BibRecordSummary
+ } from '@eg/share/catalog/bib-record.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
@Component({
selector: 'eg-bib-summary',
export class BibSummaryComponent implements OnInit {
initDone = false;
- expandDisplay = true;
- @Input() set expand(e: boolean) {
- this.expandDisplay = e;
+
+ // True / false if the display is vertically expanded
+ private _exp: boolean;
+ set expand(e: boolean) {
+ this._exp = e;
+ if (this.initDone) {
+ this.saveExpandState();
+ }
}
+ get expand(): boolean { return this._exp; }
// If provided, the record will be fetched by the component.
@Input() recordId: number;
constructor(
private bib: BibRecordService,
- private cat: CatalogService,
- private net: NetService,
private org: OrgService,
- private pcrud: PcrudService
+ private store: ServerStoreService
) {}
ngOnInit() {
- this.initDone = true;
+
if (this.summary) {
this.summary.getBibCallNumber();
} else {
this.loadSummary();
}
}
+
+ this.store.getItem('eg.cat.record.summary.collapse')
+ .then(value => this.expand = !value)
+ .then(() => this.initDone = true);
+ }
+
+ saveExpandState() {
+ this.store.setItem('eg.cat.record.summary.collapse', !this.expand);
}
loadSummary(): void {
--- /dev/null
+<eg-string #successMsg text="Successfully Modified Copy Alerts" i18n-text></eg-string>
+<eg-string #errorMsg text="Failed To Modify Copy Alerts" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header">
+ <h4 class="modal-title">
+ <ng-container *ngIf="mode == 'create'">
+ <span i18n>Adding alerts for {{copies.length}} item(s).</span>
+ </ng-container>
+ <ng-container *ngIf="mode == 'manage'">
+ <span i18n>Managing alerts for item {{copies[0].barcode()}}</span>
+ </ng-container>
+ <span i18n></span>
+ </h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body p-4 form-validated">
+ <div class="row mt-2 p-2 rounded border border-success">
+ <div class="col-lg-4">
+ <eg-combobox [entries]="alertTypes"
+ i18n-placeholder placeholder="New Alert Type..."
+ [required]="true"
+ (onChange)="newAlert.alert_type($event ? $event.id : null)">
+ </eg-combobox>
+ </div>
+ <div class="col-lg-5">
+ <textarea class="form-control" rows="2"
+ i18n-placeholder placeholder="New Alert Note..."
+ (ngModelChange)="newAlert.note($event)" [ngModel]="newAlert.note()">
+ </textarea>
+ </div>
+ <div class="col-lg-3">
+ <div class="d-flex flex-column">
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox"
+ [ngModel]="newAlert.temp() == 't'"
+ (ngModelChange)="newAlert.temp($event ? 't' : 'f')"
+ id="new-alert-temporary">
+ <label class="form-check-label" for="new-alert-temporary" i18n>
+ Temporary?
+ </label>
+ </div>
+ <div class="pt-2">
+ <button class="btn btn-success" (click)="addNew()" i18n>
+ Add New
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <ng-container *ngIf="mode == 'manage'">
+ <!-- in manage mode list all of the alerts linked to the copy -->
+ <div class="row mt-2"
+ *ngFor="let alert of copy.copy_alerts()">
+ <div class="col-lg-12 pb-2"><hr/></div>
+ <div class="col-lg-4">
+ <eg-combobox [entries]="alertTypes" [startId]="alert.alert_type()"
+ i18n-placeholder placeholder="Alert Type..."
+ [required]="true"
+ (onChange)="alert.alert_type($event ? $event.id : null); alert.ischanged(true)">
+ </eg-combobox>
+ <div class="pl-2 pt-2" i18n>
+ Added: {{alert.create_time() | date:'shortDate'}}
+ </div>
+ </div>
+ <div class="col-lg-5">
+ <textarea class="form-control" rows="2"
+ i18n-placeholder placeholder="Alert Note..."
+ (ngModelChange)="alert.note($event); alert.ischanged(true)"
+ [ngModel]="alert.note()">
+ </textarea>
+ </div>
+ <div class="col-lg-3">
+ <div class="d-flex flex-column">
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox"
+ [ngModel]="alert.temp() == 't'"
+ (ngModelChange)="alert.temp($event ? 't' : 'f'); alert.ischanged(true)"
+ id="alert-temporary-{{alert.id()}}">
+ <label class="form-check-label" for="alert-temporary-{{alert.id()}}" i18n>
+ Temporary?
+ </label>
+ </div>
+ <div class="form-check pt-2">
+ <input class="form-check-input" type="checkbox"
+ [ngModel]="alert.ack_time() != null"
+ (ngModelChange)="alert.ack_time($event ? 'now' : null); alert.ischanged(true)"
+ id="alert-temporary-{{alert.id()}}">
+ <label class="form-check-label" for="alert-temporary-{{alert.id()}}" i18n>
+ Clear?
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary"
+ (click)="dismiss('canceled')" i18n>Close</button>
+ <ng-container *ngIf="mode == 'manage'">
+ <button class="btn btn-success mr-2"
+ (click)="applyChanges()" i18n>Apply Changes</button>
+ </ng-container>
+ </div>
+</ng-template>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {NetService} from '@eg/core/net.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+
+/**
+ * Dialog for managing copy alerts.
+ */
+
+@Component({
+ selector: 'eg-copy-alerts-dialog',
+ templateUrl: 'copy-alerts-dialog.component.html'
+})
+
+export class CopyAlertsDialogComponent
+ extends DialogComponent implements OnInit {
+
+ _copyIds: number[];
+ @Input() set copyIds(ids: number[]) {
+ this._copyIds = [].concat(ids);
+ }
+ get copyIds(): number[] {
+ return this._copyIds;
+ }
+
+ _mode: string; // create | manage
+ @Input() set mode(m: string) {
+ this._mode = m;
+ }
+ get mode(): string {
+ return this._mode;
+ }
+
+ // In 'create' mode, we may be adding notes to multiple copies.
+ copies: IdlObject[];
+ // In 'manage' mode we only handle a single copy.
+ copy: IdlObject;
+ alertTypes: ComboboxEntry[];
+ newAlert: IdlObject;
+ changesMade: boolean;
+
+ @ViewChild('successMsg') private successMsg: StringComponent;
+ @ViewChild('errorMsg') private errorMsg: StringComponent;
+ @ViewChild('confirmDeleteDialog')
+ private confirmDeleteDialog: ConfirmDialogComponent;
+
+ constructor(
+ private modal: NgbModal, // required for passing to parent
+ private toast: ToastService,
+ private net: NetService,
+ private idl: IdlService,
+ private pcrud: PcrudService,
+ private org: OrgService,
+ private auth: AuthService) {
+ super(modal); // required for subclassing
+ this.copyIds = [];
+ this.copies = [];
+ }
+
+ ngOnInit() {}
+
+ /**
+ * Fetch the item/record, then open the dialog.
+ * Dialog promise resolves with true/false indicating whether
+ * the mark-damanged action occured or was dismissed.
+ */
+ async open(args: NgbModalOptions): Promise<boolean> {
+ this.copy = null;
+ this.copies = [];
+ this.newAlert = this.idl.create('aca');
+ this.newAlert.create_staff(this.auth.user().id());
+
+ if (this.copyIds.length === 0) {
+ return Promise.reject('copy ID required');
+ }
+
+ // In manage mode, we can only manage a single copy.
+ // But in create mode, we can add alerts to multiple copies.
+
+ if (this.mode === 'manage') {
+ if (this.copyIds.length > 1) {
+ console.warn('Attempt to manage alerts for multiple copies.');
+ this.copyIds = [this.copyIds[0]];
+ }
+ }
+
+ await this.getAlertTypes();
+ await this.getCopies();
+ if (this.mode === 'manage') {
+ await this.getCopyAlerts();
+ }
+ return super.open(args);
+ }
+
+ async getAlertTypes(): Promise<any> {
+ if (this.alertTypes) {
+ return Promise.resolve();
+ }
+ return this.pcrud.retrieveAll('ccat',
+ { active: true,
+ scope_org: this.org.ancestors(this.auth.user().ws_ou(), true)
+ }, {atomic: true}
+ ).toPromise().then(alerts => {
+ this.alertTypes = alerts.map(a => ({id: a.id(), label: a.name()}));
+ });
+ }
+
+ async getCopies(): Promise<any> {
+ return this.pcrud.search('acp', {id: this.copyIds}, {}, {atomic: true})
+ .toPromise().then(copies => {
+ this.copies = copies;
+ copies.forEach(c => c.copy_alerts([]));
+ if (this.mode === 'manage') {
+ this.copy = copies[0];
+ }
+ });
+ }
+
+ // Copy alerts for the selected copies which have not been
+ // acknowledged by staff and are within org unit range of
+ // the alert type.
+ async getCopyAlerts(): Promise<any> {
+ const copyIds = this.copies.map(c => c.id());
+ const typeIds = this.alertTypes.map(a => a.id);
+
+ return this.pcrud.search('aca',
+ {copy: copyIds, ack_time: null, alert_type: typeIds},
+ {}, {atomic: true})
+ .toPromise().then(alerts => {
+ alerts.forEach(a => {
+ const copy = this.copies.filter(c => c.id() === a.copy())[0];
+ copy.copy_alerts().push(a);
+ });
+ });
+ }
+
+ // Add the in-progress new note to all copies.
+ addNew() {
+ if (!this.newAlert.alert_type()) { return; }
+
+ const alerts: IdlObject[] = [];
+ this.copies.forEach(c => {
+ const a = this.idl.clone(this.newAlert);
+ a.copy(c.id());
+ alerts.push(a);
+ });
+
+ this.pcrud.create(alerts).toPromise().then(
+ newAlert => {
+ this.successMsg.current().then(msg => this.toast.success(msg));
+ this.changesMade = true;
+ if (this.mode === 'create') {
+ // In create mode, we assume the user wants to create
+ // a single alert and be done with it.
+ this.close(this.changesMade);
+ } else {
+ // Otherwise, add the alert to the copy
+ this.copy.copy_alerts().push(newAlert);
+ }
+ },
+ err => {
+ this.errorMsg.current().then(msg => this.toast.danger(msg))
+ }
+ );
+ }
+
+ applyChanges() {
+ const alerts = this.copy.copy_alerts().filter(a => a.ischanged());
+ if (alerts.length === 0) { return ;}
+ this.pcrud.update(alerts).toPromise().then(
+ ok => this.successMsg.current().then(msg => this.toast.success(msg)),
+ err => this.errorMsg.current().then(msg => this.toast.danger(msg))
+ )
+ }
+}
+
import {HoldingsService} from './holdings.service';
import {MarkDamagedDialogComponent} from './mark-damaged-dialog.component';
import {MarkMissingDialogComponent} from './mark-missing-dialog.component';
+import {CopyAlertsDialogComponent} from './copy-alerts-dialog.component';
+import {ReplaceBarcodeDialogComponent} from './replace-barcode-dialog.component';
@NgModule({
declarations: [
MarkDamagedDialogComponent,
- MarkMissingDialogComponent
+ MarkMissingDialogComponent,
+ CopyAlertsDialogComponent,
+ ReplaceBarcodeDialogComponent
],
imports: [
StaffCommonModule
],
exports: [
MarkDamagedDialogComponent,
- MarkMissingDialogComponent
+ MarkMissingDialogComponent,
+ CopyAlertsDialogComponent,
+ ReplaceBarcodeDialogComponent
],
providers: [
HoldingsService
// Open the holdings editor UI in a new browser window/tab.
spawnAddHoldingsUi(
- recordId: number, // Bib record ID
- addToVols: number[] = [], // Add copies to existing volumes
- volumeData: NewVolumeData[] = []) { // Creating new volumes
+ recordId: number, // Bib record ID
+ addToVols?: number[], // Add copies to / modify existing vols
+ volumeData?: NewVolumeData[], // Creating new volumes
+ hideCopies?: boolean) { // Hide the copy edit pane
const raw: any[] = [];
record_id: recordId,
raw: raw,
hide_vols : false,
- hide_copies : false
+ hide_copies : hideCopies ? true : false
}).then(key => {
if (!key) {
console.error('Could not create holds cache key!');
--- /dev/null
+
+
+<eg-string #successMsg
+ text="Successfully Replaced Barcode" i18n-text></eg-string>
+<eg-string #errorMsg
+ text="Failed To Replace Barcode" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title">
+ <span i18n>Replace Item Barcode</span>
+ </h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <div class="row">
+ <div class="col-lg-4" i18n>Replacing barcode</div>
+ <div class="col-lg-8 font-weight-bold">{{copy.barcode()}}</div>
+ </div>
+ <div class="row pt-2 form-validated">
+ <div class="col-lg-4" i18n>
+ <label for="new-barcode-intput">New Barcode:</label>
+ </div>
+ <div class="col-log-8">
+ <input type="text" class="form-control" [required]="true"
+ [(ngModel)]="newBarcode" (keyup)="barcodeExists=false"
+ id="new-barcode-input"/>
+ </div>
+ </div>
+ <div class="row d-flex pt-2 justify-content-center" *ngIf="barcodeExists">
+ <div class="alert alert-danger" i18n>
+ Barcode <span class="font-weight-bold">{{newBarcode}}</span> is already in use.
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <ng-container>
+ <button type="button" class="btn btn-warning"
+ (click)="dismiss('canceled')" i18n>Cancel</button>
+ <button type="button" class="btn btn-success"
+ (click)="replaceOneBarcode()" [disabled]="!newBarcode" i18n>
+ Replace Barcode
+ </button>
+ </ng-container>
+ </div>
+ </ng-template>
+
\ No newline at end of file
--- /dev/null
+import {Component, OnInit, Input, ViewChild, Renderer2} from '@angular/core';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {StringComponent} from '@eg/share/string/string.component';
+
+
+/**
+ * Dialog for marking items missing.
+ */
+
+@Component({
+ selector: 'eg-replace-barcode-dialog',
+ templateUrl: 'replace-barcode-dialog.component.html'
+})
+
+export class ReplaceBarcodeDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() copyIds: number[];
+ ids: number[]; // copy of list so we can pop()
+
+ copy: IdlObject;
+ newBarcode: string;
+ barcodeExists: boolean;
+
+ numSucceeded: number;
+ numFailed: number;
+
+ @ViewChild('successMsg')
+ private successMsg: StringComponent;
+
+ @ViewChild('errorMsg')
+ private errorMsg: StringComponent;
+
+ constructor(
+ private modal: NgbModal, // required for passing to parent
+ private toast: ToastService,
+ private net: NetService,
+ private pcrud: PcrudService,
+ private evt: EventService,
+ private renderer: Renderer2,
+ private auth: AuthService) {
+ super(modal); // required for subclassing
+ }
+
+ ngOnInit() {}
+
+ async open(args: NgbModalOptions): Promise<boolean> {
+ this.ids = [].concat(this.copyIds);
+ this.numSucceeded = 0;
+ this.numFailed = 0;
+
+ await this.getNextCopy();
+ setTimeout(() =>
+ // Give the dialog a chance to render
+ this.renderer.selectRootElement('#new-barcode-input').focus()
+ );
+ return super.open(args);
+ }
+
+ async getNextCopy(): Promise<any> {
+
+ if (this.ids.length === 0) {
+ this.close(this.numSucceeded > 0);
+ return Promise.resolve();
+ }
+
+ this.newBarcode = '';
+
+ const id = this.ids.pop();
+
+ return this.pcrud.retrieve('acp', id)
+ .toPromise().then(c => this.copy = c);
+ }
+
+ async replaceOneBarcode(): Promise<any> {
+ this.barcodeExists = false;
+
+ // First see if the barcode is in use
+ return this.pcrud.search('acp', {deleted: 'f', barcode: this.newBarcode})
+ .toPromise().then(async (existing) => {
+ if (existing) {
+ this.barcodeExists = true;
+ return;
+ }
+
+ this.copy.barcode(this.newBarcode);
+ this.pcrud.update(this.copy).toPromise().then(
+ async (ok) => {
+ this.numSucceeded++;
+ this.toast.success(await this.successMsg.current());
+ this.getNextCopy();
+ },
+ async (err) => {
+ this.numFailed++;
+ console.error('Replace barcode failed: ', err);
+ this.toast.warning(await this.errorMsg.current());
+ }
+ )
+ })
+ }
+}
+
+
+
}
this.markDamagedDialog.copyId = ids.pop();
- this.markDamagedDialog.open({size: 'lg'}).then(
+ return this.markDamagedDialog.open({size: 'lg'}).then(
ok => {
if (ok) { rowsModified = true; }
return markNext(ids);
}
+
+