<div class="d-flex">
<input type="text"
class="form-control"
+ [id]="domId"
[ngClass]="{
'text-success font-italic font-weight-bold': selected && selected.freetext,
'form-control-sm': smallFormControl
}]
})
export class ComboboxComponent implements ControlValueAccessor, OnInit {
+ static domIdAuto = 0;
selected: ComboboxEntry;
click$: Subject<string>;
@ViewChild('instance', { static: true }) instance: NgbTypeahead;
+ @Input() domId = 'eg-combobox-' + ComboboxComponent.domIdAuto++;
+
// Applies a name attribute to the input.
// Useful in forms.
@Input() name: string;
<eg-string #unsetString text="<Unset>" i18n-text></eg-string>
<eg-combobox #comboBox
+ [domId]="domId"
[startId]="startId"
[displayTemplate]="displayTemplate"
(onChange)="cboxChanged($event)"
})
export class ItemLocationSelectComponent
implements OnInit, AfterViewInit, ControlValueAccessor {
+ static domIdAuto = 0;
// Limit copy locations to those owned at or above org units where
// the user has work permissions for the provided permission code.
@Input() required: boolean;
+ @Input() domId = 'eg-item-location-select-' +
+ ItemLocationSelectComponent.domIdAuto++;
+
@ViewChild('comboBox', {static: false}) comboBox: ComboboxComponent;
@ViewChild('unsetString', {static: false}) unsetString: StringComponent;
templateUrl: './org-select.component.html'
})
export class OrgSelectComponent implements OnInit {
+ static domIdAuto = 0;
selected: OrgDisplay;
hidden: number[] = [];
@Input() stickySetting: string;
// ID to display in the DOM for this selector
- @Input() domId = '';
+ @Input() domId = 'eg-org-select-' + OrgSelectComponent.domIdAuto++;
// Org unit field displayed in the selector
@Input() displayField = 'shortname';
+<!-- We ask this question a lot. Here's a handy template -->
+<ng-template #yesNoSelect let-field="field">
+ <eg-combobox domId="{{field}}-input"
+ [required]="true" [ngModel]="values['field']"
+ (ngModelChange)="values[field] = $event ? $event.id : null">
+ <eg-combobox-entry entryId="t" entryLabel="Yes" i18n-entryLabel>
+ </eg-combobox-entry>
+ <eg-combobox-entry entryId="f" entryLabel="No" i18n-entryLabel>
+ </eg-combobox-entry>
+ </eg-combobox>
+</ng-template>
+
+
<!-- Copy Templates -->
-<div class="row">
+<div class="row border rounded border-dark pt-2 pb-2 bg-faint">
+ <div class="col-lg-1 font-weight-bold" i18n>Templates:</div>
+ <div class="col-lg-4">
+ <eg-combobox domId="template-select" #copyTemplateCbox></eg-combobox>
+ </div>
+ <div class="col-lg-7 d-flex">
+ <button class="btn btn-outline-dark mr-2" (click)="applyTemplate()" i18n>Apply</button>
+ <button class="btn btn-outline-dark mr-2" (click)="saveTemplate()" i18n>Save</button>
+ <button class="btn btn-outline-dark mr-2" (click)="importTemplate()" i18n>Import</button>
+ <button class="btn btn-outline-dark mr-2" (click)="exportTemplate()" i18n>Export</button>
+ <div class="flex-1"> </div>
+ <button class="btn btn-outline-dark mr-2"
+ (click)="copyTemplateCbox.selectedId = null" i18n>Clear</button>
+ <button class="btn btn-outline-danger mr-2" (click)="deleteTemplate()" i18n>Delete Template</button>
+ </div>
</div>
<div>
<ng-template #locationTemplate>
<eg-item-location-select (valueChange)="values['location'] = $event"
- [required]="true" permFilter="UPDATE_COPY">
+ domId='location-input' [required]="true" permFilter="UPDATE_COPY">
</eg-item-location-select>
</ng-template>
-
<eg-batch-item-attr label="Location / Collection" i18n-label
+ editInputDomId="location-input"
[editTemplate]="locationTemplate"
[labelCounts]="itemAttrCounts('location')"
(changesSaved)="applyCopyValue('location')">
<div>
<ng-template #circLibTemplate>
<eg-org-select
+ domId="circ-lib-input"
(onChange)="values['circ_lib'] = $event ? $event.id() : null"
[limitPerms]="['UPDATE_COPY']">
</eg-org-select>
</ng-template>
-
<eg-batch-item-attr label="Circulating Library" i18n-label
+ editInputDomId="circ-lib-input"
[editTemplate]="circLibTemplate"
[labelCounts]="itemAttrCounts('circ_lib')"
(changesSaved)="circLibChanged()">
<div>
<ng-template #owningLibTemplate>
<eg-org-select
+ domId="owning-lib-input"
(onChange)="values['owning_lib'] = $event ? $event.id() : null"
[limitPerms]="['UPDATE_COPY']">
</eg-org-select>
</ng-template>
-
<eg-batch-item-attr label="Owning Library" i18n-label
+ editInputDomId="owning-lib-input"
[editTemplate]="owningLibTemplate"
[labelCounts]="itemAttrCounts('owning_lib')"
(changesSaved)="owningLibChanged()">
<div>
<ng-template #copyNumberTemplate>
<input type="number" class="form-control"
- [(ngModel)]="values['copy_number']"/>
+ id="copy-number-input" [(ngModel)]="values['copy_number']"/>
</ng-template>
-
<eg-batch-item-attr label="Copy Number" i18n-label
+ editInputDomId="copy-number-input"
[emptyIsUnset]="true"
[editTemplate]="copyNumberTemplate"
[labelCounts]="itemAttrCounts('copy_number')"
<div class="p-1"><h4 class="font-weight-bold" i18n>Circulation</h4></div>
<div>
- <ng-template #circulateTemplate>
- <select class="form-control" [(ngModel)]="values['circulate']">
- <option value="yes" i18n>Yes</option>
- <option value="no" i18n>No</option>
- </select>
- </ng-template>
+ <ng-template #circulateTemplate>
+ <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'circulate'}">
+ </ng-container>
+ </ng-template>
<eg-batch-item-attr label="Circulate" i18n-label
displayAs="bool"
+ editInputDomId="circulate-input"
[editTemplate]="circulateTemplate"
[labelCounts]="itemAttrCounts('circulate')"
- (changesSaved)="applyCopyValue('circulate',
- values['circulate'] === 'yes' ? 't': 'f')">
+ (changesSaved)="applyCopyValue('circulate')">
</eg-batch-item-attr>
</div>
<div>
- <ng-template #holdableTemplate>
- <select class="form-control" [(ngModel)]="values['holdable']">
- <option value="yes" i18n>Yes</option>
- <option value="no" i18n>No</option>
- </select>
- </ng-template>
+ <ng-template #holdableTemplate>
+ <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'holdable'}">
+ </ng-container>
+ </ng-template>
<eg-batch-item-attr label="Holdable" i18n-label
displayAs="bool"
+ editInputDomId="holdable-input"
[editTemplate]="holdableTemplate"
[labelCounts]="itemAttrCounts('holdable')"
- (changesSaved)="applyCopyValue('holdable',
- values['holdable'] === 'yes' ? 't': 'f')">
+ (changesSaved)="applyCopyValue('holdable')">
</eg-batch-item-attr>
</div>
<div>
- <ng-template #ageProtectTemplate>
- <select class="form-control" [(ngModel)]="values['age_protect']">
+ <ng-template #ageProtectTemplate>
+ <select class="form-control"
+ id="age-protect-input" [(ngModel)]="values['age_protect']">
<option [value]="null" i18n><Unset></option>
- <option *ngFor="let rule of ageProtectRules"
+ <option *ngFor="let rule of ageProtectRules"
value="{{rule.id()}}">{{rule.name()}}</option>
- </select>
- </ng-template>
+ </select>
+ </ng-template>
<eg-batch-item-attr label="Aged-Based Hold Protection" i18n-label
+ editInputDomId="age-protect-input"
[emptyIsUnset]="true"
[editTemplate]="ageProtectTemplate"
[labelCounts]="itemAttrCounts('age_protect')"
</div>
<div>
- <ng-template #floatingTemplate>
- <select class="form-control" [(ngModel)]="values['floating']">
+ <ng-template #floatingTemplate>
+ <select class="form-control"
+ id="floating-input" [(ngModel)]="values['floating']">
<option [value]="null" i18n><Unset></option>
- <option *ngFor="let grp of floatingGroups"
+ <option *ngFor="let grp of floatingGroups"
value="{{grp.id()}}">{{grp.name()}}</option>
- </select>
- </ng-template>
+ </select>
+ </ng-template>
<eg-batch-item-attr label="Floating" i18n-label
+ editInputDomId="floating-input"
[emptyIsUnset]="true"
[editTemplate]="floatingTemplate"
[labelCounts]="itemAttrCounts('floating')"
<eg-string #loanDurationNormal i18n-text text="Normal"></eg-string>
<eg-string #loanDurationLong i18n-text text="Long"></eg-string>
- <ng-template #loanDurationTemplate>
- <select class="form-control" [(ngModel)]="values['loan_duration']">
+ <ng-template #loanDurationTemplate>
+ <select class="form-control"
+ id="loan-duration-input" [(ngModel)]="values['loan_duration']">
<option value="1" i18n>{{loanDurationShort.text}}</option>
<option value="2" i18n>{{loanDurationNormal.text}}</option>
<option value="3" i18n>{{loanDurationLong.text}}</option>
- </select>
- </ng-template>
+ </select>
+ </ng-template>
<eg-batch-item-attr label="Loan Duration" i18n-label
+ editInputDomId="loan-duration-input"
[emptyIsUnset]="true"
[editTemplate]="loanDurationTemplate"
[labelCounts]="itemAttrCounts('loan_duration')"
<eg-string #fineLevelNormal i18n-text text="Normal"></eg-string>
<eg-string #fineLevelHigh i18n-text text="High"></eg-string>
- <ng-template #fineLevelTemplate>
- <select class="form-control" [(ngModel)]="values['fine_level']">
+ <ng-template #fineLevelTemplate>
+ <select class="form-control"
+ id="fine-level-input" [(ngModel)]="values['fine_level']">
<option value="1" i18n>{{fineLevelLow.text}}</option>
<option value="2" i18n>{{fineLevelNormal.text}}</option>
<option value="3" i18n>{{fineLevelHigh.text}}</option>
- </select>
- </ng-template>
+ </select>
+ </ng-template>
<eg-batch-item-attr label="Fine Level" i18n-label
+ editInputDomId="fine-level-input"
[emptyIsUnset]="true"
[editTemplate]="fineLevelTemplate"
[labelCounts]="itemAttrCounts('fine_level')"
</div>
<div>
- <ng-template #ageProtectTemplate>
- <select class="form-control" [(ngModel)]="values['age_protect']">
+ <ng-template #ageProtectTemplate>
+ <select class="form-control" [(ngModel)]="values['age_protect']">
<option [value]="null" i18n><Unset></option>
- <option *ngFor="let rule of ageProtectRules"
+ <option *ngFor="let rule of ageProtectRules"
value="{{rule.id()}}">{{rule.name()}}</option>
- </select>
- </ng-template>
+ </select>
+ </ng-template>
<eg-batch-item-attr label="Aged-Based Hold Protection" i18n-label
[emptyIsUnset]="true"
[editTemplate]="ageProtectTemplate"
</div>
<div>
- <ng-template #circAsTypeTemplate>
- <select class="form-control" [(ngModel)]="values['circ_as_type']">
+ <ng-template #circAsTypeTemplate>
+ <select class="form-control" [(ngModel)]="values['circ_as_type']">
<option [value]="null" i18n><Unset></option>
- <option *ngFor="let map of itemTypeMaps"
+ <option *ngFor="let map of itemTypeMaps"
value="{{map.code()}}">{{map.value()}}</option>
- </select>
- </ng-template>
+ </select>
+ </ng-template>
<eg-batch-item-attr label="Circulate as Type" i18n-label
[emptyIsUnset]="true"
[editTemplate]="circAsTypeTemplate"
</div>
<div>
- <ng-template #circModifierTemplate>
- <select class="form-control" [(ngModel)]="values['circ_modifier']">
+ <ng-template #circModifierTemplate>
+ <select class="form-control" [(ngModel)]="values['circ_modifier']">
<option [value]="null" i18n><Unset></option>
- <option *ngFor="let mod of circModifiers"
+ <option *ngFor="let mod of circModifiers"
value="{{mod.code()}}">{{mod.name()}}</option>
- </select>
- </ng-template>
+ </select>
+ </ng-template>
<eg-batch-item-attr label="Circulion Modifier" i18n-label
[emptyIsUnset]="true"
[editTemplate]="circModifierTemplate"
<!-- COLUMN 4 -->
<div class="flex-1 p-1">
-
<div class="p-1"><h4 class="font-weight-bold" i18n>Miscellaneous</h4></div>
+ <!-- Adding this for sites that still use alert messages (we do)
<div>
- <ng-template #alertMessageTemplate>
- <textarea rows="3" class="form-control"
+ <ng-template #alertMessageTemplate>
+ <textarea rows="3" class="form-control" id="alert-message-input"
[(ngModel)]="values['alert_message']">
</textarea>
- </ng-template>
+ </ng-template>
<eg-batch-item-attr label="Alert Message" i18n-label
+ editInputDomId="alert-message-input"
[emptyIsUnset]="true"
[editTemplate]="alertMessageTemplate"
[labelCounts]="itemAttrCounts('alert_message')"
(changesSaved)="applyCopyValue('alert_message')">
</eg-batch-item-attr>
</div>
+ -->
+
+ <div class="border rounded m-1">
+ <eg-copy-alerts-dialog #copyAlertsDialog></eg-copy-alerts-dialog>
+ <div class="batch-header font-weight-bold p-2" i18n>Add Item Alerts</div>
+ <div class="p-1">
+ <button class="btn btn-outline-dark" (click)="openCopyAlerts()" i18n>
+ Item Alerts
+ </button>
+ </div>
+ </div>
<div>
- <ng-template #depositTemplate>
- <select class="form-control" [(ngModel)]="values['deposit']">
- <option value="yes" i18n>Yes</option>
- <option value="no" i18n>No</option>
- </select>
- </ng-template>
+ <ng-template #depositTemplate>
+ <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'deposit'}">
+ </ng-container>
+ </ng-template>
<eg-batch-item-attr label="Deposit" i18n-label
displayAs="bool"
+ editInputDomId="deposit-input"
[editTemplate]="depositTemplate"
[labelCounts]="itemAttrCounts('deposit')"
- (changesSaved)="applyCopyValue('deposit',
- values['deposit'] === 'yes' ? 't': 'f')">
+ (changesSaved)="applyCopyValue('deposit')">
</eg-batch-item-attr>
</div>
<div>
- <ng-template #depositAmountTemplate>
- <input type="number" class="form-control"
- [(ngModel)]="values['deposit_amount']"/>
- </ng-template>
+ <ng-template #depositAmountTemplate>
+ <input type="number" class="form-control"
+ id="deposit-amount-input" [(ngModel)]="values['deposit_amount']"/>
+ </ng-template>
<eg-batch-item-attr label="Deposit Amount" i18n-label
displayAs="currency"
+ editInputDomId="deposit-amount-input"
[editTemplate]="depositAmountTemplate"
[labelCounts]="itemAttrCounts('deposit_amount')"
(changesSaved)="applyCopyValue('deposit_amount')">
</div>
<div>
- <ng-template #priceTemplate>
- <input type="number" class="form-control"
- [(ngModel)]="values['price']"/>
- </ng-template>
+ <ng-template #priceTemplate>
+ <input type="number" class="form-control"
+ id="price-input" [(ngModel)]="values['price']"/>
+ </ng-template>
<eg-batch-item-attr label="Price" i18n-label
displayAs="currency"
+ editInputDomId="price-input"
[editTemplate]="priceTemplate"
[labelCounts]="itemAttrCounts('price')"
(changesSaved)="applyCopyValue('price')">
</div>
<div>
- <ng-template #opacVisibleTemplate>
- <select class="form-control" [(ngModel)]="values['opac_visible']">
- <option value="yes" i18n>Yes</option>
- <option value="no" i18n>No</option>
- </select>
- </ng-template>
+ <ng-template #opacVisibleTemplate>
+ <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'opac_visible'}">
+ </ng-container>
+ </ng-template>
<eg-batch-item-attr label="OPAC Visible" i18n-label
displayAs="bool"
+ editInputDomId="opac_visible-input"
[editTemplate]="opacVisibleTemplate"
[labelCounts]="itemAttrCounts('opac_visible')"
- (changesSaved)="applyCopyValue('opac_visible',
- values['opac_visible'] === 'yes' ? 't': 'f')">
+ (changesSaved)="applyCopyValue('opac_visible')">
</eg-batch-item-attr>
</div>
<div>
- <ng-template #refTemplate>
- <select class="form-control" [(ngModel)]="values['ref']">
- <option value="yes" i18n>Yes</option>
- <option value="no" i18n>No</option>
- </select>
- </ng-template>
+ <ng-template #refTemplate>
+ <ng-container *ngTemplateOutlet="yesNoSelect;context:{field:'ref'}">
+ </ng-container>
+ </ng-template>
<eg-batch-item-attr label="Reference" i18n-label
displayAs="bool"
+ editInputDomId="ref-input"
[editTemplate]="refTemplate"
[labelCounts]="itemAttrCounts('ref')"
- (changesSaved)="applyCopyValue(
- 'ref', values['ref'] === 'yes' ? 't': 'f')">
+ (changesSaved)="applyCopyValue('ref')">
</eg-batch-item-attr>
</div>
<div>
- <ng-template #costTemplate>
- <input type="number" class="form-control"
- [(ngModel)]="values['cost']"/>
- </ng-template>
+ <ng-template #costTemplate>
+ <input type="number" class="form-control"
+ id="cost-input" [(ngModel)]="values['cost']"/>
+ </ng-template>
<eg-batch-item-attr label="Acquisition Cost" i18n-label
- [emptyIsUnset]="true"
displayAs="currency"
+ editInputDomId="cost-input"
+ [emptyIsUnset]="true"
[editTemplate]="costTemplate"
[labelCounts]="itemAttrCounts('cost')"
(changesSaved)="applyCopyValue('cost')">
<eg-string #mintConditionYes i18n-text text="Good"></eg-string>
<eg-string #mintConditionNo i18n-text text="Damaged"></eg-string>
- <ng-template #mintConditionTemplate>
- <select class="form-control" [(ngModel)]="values['mint_condition']">
+ <ng-template #mintConditionTemplate>
+ <select class="form-control"
+ id="mint-condition-input" [(ngModel)]="values['mint_condition']">
<option value="t" i18n>{{mintConditionYes.text}}</option>
<option value="f" i18n>{{mintConditionNo.text}}</option>
- </select>
- </ng-template>
+ </select>
+ </ng-template>
<eg-batch-item-attr label="Quality" i18n-label
+ editInputDomId="mint-condition-input"
[emptyIsUnset]="true"
[editTemplate]="mintConditionTemplate"
[labelCounts]="itemAttrCounts('mint_condition')"
<!-- COLUMN 5 -->
<div class="flex-1 p-1">
<div class="p-1"><h4 class="font-weight-bold" i18n>Statistics</h4></div>
+
+ <div *ngFor="let cat of statCats(); let idx = index">
+ <ng-template #statCatTemplate>
+ <eg-combobox domId="stat-cat-input-{{idx}}"
+ (ngModelChange)="statCatValues[cat.id()] = $event ? $event.id : null"
+ [ngModel]="statCatValues[cat.id()]">
+ <eg-combobox-entry *ngFor="let entry of cat.entries()"
+ [entryId]="entry.id()" [entryLabel]="entry.value()">
+ </eg-combobox-entry>
+ </eg-combobox>
+ </ng-template>
+ <eg-batch-item-attr label="{{cat.name()}} ({{orgSn(cat.owner())}})" i18n-label
+ editInputDomId="stat-cat-input-{{idx}}"
+ [emptyIsUnset]="true"
+ [editTemplate]="statCatTemplate"
+ [labelCounts]="statCatCounts(cat.id())"
+ (changesSaved)="statCatChanged(cat.id())">
+ </eg-batch-item-attr>
+ </div>
+
</div>
</div>
import {IdlObject, IdlService} from '@eg/core/idl.service';
import {EventService} from '@eg/core/event.service';
import {OrgService} from '@eg/core/org.service';
+import {StoreService} from '@eg/core/store.service';
import {NetService} from '@eg/core/net.service';
import {AuthService} from '@eg/core/auth.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {VolCopyService} from './volcopy.service';
import {FormatService} from '@eg/core/format.service';
import {StringComponent} from '@eg/share/string/string.component';
+import {CopyAlertsDialogComponent
+ } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
+import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
@Component({
selector: 'eg-copy-attrs',
- templateUrl: 'copy-attrs.component.html'
+ templateUrl: 'copy-attrs.component.html',
+
+ // Match the header of the batch attrs component
+ styles: [
+ `.batch-header {background-color: #EBF4FA;}`,
+ `.template-row {background-color: #EBF4FA;}`
+ ]
})
export class CopyAttrsComponent implements OnInit, AfterViewInit {
// Some values are scalar, some IdlObjects depending on copy fleshyness.
values: {[field: string]: any} = {};
- ageProtectRules: IdlObject[] = [];
- floatingGroups: IdlObject[] = [];
- itemTypeMaps: IdlObject[] = [];
- circModifiers: IdlObject[] = [];
+ // Map of stat ID to entry ID.
+ statCatValues: {[statId: number]: number} = {};
loanDurationLabelMap: {[level: number]: string} = {};
fineLevelLabelMap: {[level: number]: string} = {};
@ViewChild('mintConditionNo', {static: false})
mintConditionNo: StringComponent;
+ @ViewChild('copyAlertsDialog', {static: false})
+ private copyAlertsDialog: CopyAlertsDialogComponent;
+
+ @ViewChild('copyTemplateCbox', {static: false})
+ copyTemplateCbox: ComboboxComponent;
+
constructor(
private router: Router,
private route: ActivatedRoute,
private pcrud: PcrudService,
private holdings: HoldingsService,
private volcopy: VolCopyService,
- private format: FormatService
+ private format: FormatService,
+ private store: StoreService
) { }
ngOnInit() {
- this.load();
}
ngAfterViewInit() {
+ const tmpl = this.store.getLocalItem('cat.copy.last_template');
+ if (tmpl) { this.copyTemplateCbox.selectedId = tmpl; }
+
this.loanDurationLabelMap[1] = this.loanDurationShort.text;
this.loanDurationLabelMap[2] = this.loanDurationNormal.text;
this.loanDurationLabelMap[3] = this.loanDurationLong.text;
this.fineLevelLabelMap[3] = this.fineLevelHigh.text;
}
- load() {
-
- this.pcrud.retrieveAll('crahp')
- .pipe(tap(rule => this.ageProtectRules.push(rule))).toPromise()
- .then(_ => {
-
- this.ageProtectRules = this.ageProtectRules.sort(
- (a, b) => a.name() < b.name() ? -1 : 1);
-
- }).then(_ => {
-
- return this.pcrud.retrieveAll('cfg')
- .pipe(tap(rule => this.floatingGroups.push(rule))).toPromise()
-
- }).then(_ => {
-
- this.floatingGroups = this.floatingGroups.sort(
- (a, b) => a.name() < b.name() ? -1 : 1);
-
- }).then(_ => {
-
- return this.pcrud.retrieveAll('ccm')
- .pipe(tap(rule => this.circModifiers.push(rule))).toPromise()
-
- }).then(_ => {
+ statCats(): IdlObject[] {
+ return this.volcopy.statCats;
+ }
- this.circModifiers = this.circModifiers.sort(
- (a, b) => a.name() < b.name() ? -1 : 1);
- }).then(_ => {
+ orgSn(orgId: number): string {
+ return orgId ? this.org.get(orgId).shortname() : '';
+ }
- return this.pcrud.retrieveAll('citm')
- .pipe(tap(itemType => this.itemTypeMaps.push(itemType))).toPromise()
+ statCatCounts(catId: number): {[value: string]: number} {
+ catId = Number(catId);
+ const counts = {};
- }).then(_ => {
+ this.context.copyList().forEach(copy => {
+ const entry = copy.stat_cat_entries()
+ .filter(e => e.stat_cat() === catId)[0];
+
+ let value = '';
+ if (entry) {
+ if (this.volcopy.statCatEntryMap[entry.id()]) {
+ value = this.volcopy.statCatEntryMap[entry.id()].value();
+ } else {
+ // Map to a remote stat cat. Ignore.
+ return;
+ }
+ }
- this.itemTypeMaps = this.itemTypeMaps.sort(
- (a, b) => a.value() < b.value() ? -1 : 1);
+ if (counts[value] === undefined) {
+ counts[value] = 0;
+ }
+ counts[value]++;
});
+
+ return counts;
}
itemAttrCounts(field: string): {[value: string]: number} {
if (counts[value] === undefined) {
counts[value] = 0;
- };
+ }
counts[value]++;
});
' : ' + copy.call_number().label();
}
- let value = copy[field]();
+ const value = copy[field]();
if (!value && value !== 0) { return ''; }
- switch(field) {
+ switch (field) {
case 'status':
return this.volcopy.copyStatuses[value].name();
return this.org.get(value).shortname();
case 'age_protect':
- const rule = this.ageProtectRules.filter(
+ const rule = this.volcopy.ageProtectRules.filter(
r => r.id() === Number(value))[0];
return rule ? rule.name() : '';
case 'floating':
- const grp = this.floatingGroups.filter(
+ const grp = this.volcopy.floatingGroups.filter(
g => g.id() === Number(value))[0];
return grp ? grp.name() : '';
return this.fineLevelLabelMap[value];
case 'circ_as_type':
- const map = this.itemTypeMaps.filter(
+ const map = this.volcopy.itemTypeMaps.filter(
m => m.code() === value)[0];
return map ? map.value() : '';
case 'circ_modifier':
- const mod = this.circModifiers.filter(
+ const mod = this.volcopy.circModifiers.filter(
m => m.code() === value)[0];
return mod ? mod.name() : '';
owningLibChanged() {
// TODO
+ // copies.ischanged(true);
console.log('OWNING LIB ', this.values['owning_lib']);
}
+
+
+ // Create or modify a stat cat entry for each copy that does not
+ // already match the new value.
+ statCatChanged(catId: number) {
+ catId = Number(catId);
+
+ const entryId = this.statCatValues[catId];
+ this.context.copyList().forEach(copy => {
+
+ let entry = copy.stat_cat_entries()
+ .filter(e => e.stat_cat() === catId)[0];
+
+ if (entry) {
+ if (entry.id() === entryId) {
+ // Requested mapping already exists.
+ return;
+ }
+ } else {
+
+ // Copy has no entry for this stat cat yet.
+ entry = this.idl.create('asce');
+ entry.stat_cat(catId);
+ copy.stat_cat_entries().push(entry);
+ }
+
+ entry.id(entryId);
+ entry.value(this.volcopy.statCatEntryMap[entryId].value());
+
+ copy.ischanged(true);
+ });
+ }
+
+ openCopyAlerts() {
+ this.copyAlertsDialog.inPlaceMode = true;
+ this.copyAlertsDialog.mode = 'create';
+ this.copyAlertsDialog.open({size: 'lg'}).subscribe(
+ newAlert => {
+ if (newAlert) {
+ this.context.copyList().forEach(copy => {
+ const a = this.idl.clone(newAlert);
+ a.isnew(true);
+ a.copy(copy.id());
+ if (!copy.copy_alerts()) { copy.copy_alerts([]); }
+ copy.copy_alerts().push(a);
+ copy.ischanged(true);
+ });
+ }
+ }
+ );
+ }
+
+ applyTemplate() {
+ const entry = this.copyTemplateCbox.selected;
+ if (!entry) { return; }
+
+ this.store.setLocalItem('cat.copy.last_template', entry.id);
+
+ const template = this.volcopy.templates[entry.id];
+
+ Object.keys(template).forEach(field => {
+ const value = template[field];
+
+ if (value === null || value === undefined) { return; }
+
+ this.applyCopyValue(field, value);
+ });
+ }
}
import {VolCopyComponent} from './volcopy.component';
const routes: Routes = [{
- path: 'edit/item/:copy_id',
- component: VolCopyComponent
- }, {
- path: 'edit/callnumber/:vol_id',
- component: VolCopyComponent
- }, {
- path: 'edit/record/:record_id',
- component: VolCopyComponent
- }, {
- path: 'edit/session/:session',
+ path: ':tab/:target/:target_id',
component: VolCopyComponent
/*
}, {
.vol-row {
background-color: rgba(0,0,0,.03);
- border-top: 1px solid rgba(0,0,0,.125);
- border-bottom: 1px solid rgba(0,0,0,.125);
+ border-top: 1px solid #d9edf7;
+ border-bottom: 1px solid #d9edf7;
}
-
.clear-button {
border: none;
background-color: rgba(0, 0, 0, 0.0);
dialogBody="Delete {{deleteCopyCount}} Item(s)?">
</eg-confirm-dialog>
-<div class="row d-flex vol-row border border-info mb-2">
+<div class="row d-flex bg-faint mb-2 pb-1 pt-1 border border-dark rounded">
<div class="p-1" [ngStyle]="{flex: flexAt(1)}"> </div>
<div class="p-1" [ngStyle]="{flex: flexAt(2)}"> </div>
<div class="p-1" [ngStyle]="{flex: flexAt(3)}">
</ng-container>
</ng-container>
</ng-container>
+
+<hr/>
+
+<div class="form-inline">
+ <eg-org-select #newVolOrg [applyDefault]="true">
+ </eg-org-select>
+ <button class="btn btn-outline-dark ml-2"
+ (click)="addVol(newVolOrg.selectedOrg())" i18n>
+ Add Call Number
+ </button>
+</div>
+
);
});
+ if (Object.keys(this.bibParts).length === 0) { return; }
+
this.pcrud.search('bmp',
{record: Object.keys(this.bibParts), deleted: 'f'})
.subscribe(
}
}
+
+ addVol(org: IdlObject) {
+ if (!org) { return; }
+ const orgNode = this.context.findOrCreateOrgNode(org.id());
+ this.createVols(orgNode, 1);
+ }
+
existingVolCount(orgNode: HoldingsTreeNode): number {
return orgNode.children.filter(volNode => !volNode.target.isnew()).length;
}
}
barcodeChanged(copy: IdlObject, barcode: string) {
- copy.barcode(barcode);
+ // note: copy.barcode(barcode) applied via ngModel
copy.ischanged(true);
copy._dupe_barcode = false;
}
deleteOneCopy(copyNode: HoldingsTreeNode) {
-
const targetCopy = copyNode.target;
const orgNodes = this.context.orgNodes();
}
deleteOneVol(volNode: HoldingsTreeNode) {
+
let deleteVolIdx = null;
const targetVol = volNode.target;
<eg-staff-banner bannerText="Holdings Editor" i18n-bannerText></eg-staff-banner>
-<div class="row" [hidden]="!loading">
- <div class="col-lg-6 offset-lg-3">
- <eg-progress-inline #loadingProgress></eg-progress-inline>
- </div>
-</div>
-
<div class="row" *ngIf="sessionExpired">
<div class="col-lg-6 mt-4 offset-lg-3 alert alert-danger d-flex justify-content-center" i18n>
Holdings Editor Session Expired
<ng-container *ngIf="!loading && !sessionExpired">
<eg-bib-summary *ngIf="context.recordId" [recordId]="context.recordId"></eg-bib-summary>
-
- <div class="mt-3" *ngIf="!context.hideVols">
- <eg-vol-edit [context]="context"></eg-vol-edit>
- </div>
-
- <ng-container *ngIf="!context.hideVols && !context.hideCopies">
- <hr class="m-2"/>
- </ng-container>
-
- <div class="mt-3" *ngIf="!context.hideCopies">
- <eg-copy-attrs [context]="context"></eg-copy-attrs>
- </div>
- <div class="row m-2 p-2 border border-info">
- <div class="col-lg-12 d-flex">
- <div class="flex-1"> </div>
- <button class="btn btn-outline-dark"
- [disabled]="!context.isSaveable()" (click)="save()" i18n>Save</button>
- <button class="btn btn-outline-dark ml-2"
- [disabled]="!context.isSaveable()"
- (click)="save(true)" i18n>Save & Exit</button>
+ <div class="m-2"> </div>
+
+ <ngb-tabset [activeId]="tab" (tabChange)="beforeTabChange($event)">
+
+ <ngb-tab title="Holdings" i18n-title id="holdings">
+ <ng-template ngbTabContent>
+ <div class="mt-2">
+ <div class="row" [hidden]="!loading">
+ <div class="col-lg-6 offset-lg-3">
+ <eg-progress-inline #loadingProgress></eg-progress-inline>
+ </div>
+ </div>
+ <eg-vol-edit [context]="context"></eg-vol-edit>
+ </div>
+ </ng-template>
+ </ngb-tab>
+
+ <ngb-tab title="Item Attributes" i18n-title id="attrs">
+ <ng-template ngbTabContent>
+ <div class="mt-2">
+ <div class="row" [hidden]="!loading">
+ <div class="col-lg-6 offset-lg-3">
+ <eg-progress-inline #loadingProgress></eg-progress-inline>
+ </div>
+ </div>
+ </div>
+ <eg-copy-attrs [context]="context"></eg-copy-attrs>
+ </ng-template>
+ </ngb-tab>
+
+ </ngb-tabset>
+
+ <ng-container *ngIf="tab === 'holdings' || tab === 'attrs'">
+ <hr class="m-2"/>
+ <div class="row m-2 p-2 border border-dark rounded bg-faint">
+ <div class="col-lg-12 d-flex">
+ <div class="flex-1"> </div>
+ <button class="btn btn-outline-dark" (click)="save()" i18n>Save</button>
+ <button class="btn btn-outline-dark ml-2"
+ (click)="save(true)" i18n>Save & Exit</button>
+ </div>
</div>
- </div>
+ </ng-container>
</ng-container>
import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
import {AnonCacheService} from '@eg/share/util/anon-cache.service';
import {VolCopyService} from './volcopy.service';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
const COPY_FLESH = {
flesh: 1,
flesh_fields: {
- acp: ['call_number', 'location', 'parts', 'creator', 'editor']
+ acp: ['call_number', 'location', 'parts',
+ 'creator', 'editor', 'stat_cat_entries']
}
};
loading = true;
sessionExpired = false;
+ tab = 'holdings'; // holdings | attrs | config
+ target: string; // item | callnumber | record | session
+ targetId: string; // id value or session string
+
@ViewChild('loadingProgress', {static: false})
loadingProgress: ProgressInlineComponent;
) { }
ngOnInit() {
- this.context = new VolCopyContext();
- this.context.org = this.org; // inject;
-
this.route.paramMap.subscribe(
(params: ParamMap) => this.negotiateRoute(params));
}
negotiateRoute(params: ParamMap) {
- this.context.recordId = +params.get('record_id') || null;
- this.context.volId = +params.get('vol_id') || null;
- this.context.copyId = +params.get('copy_id') || null;
- this.context.session = params.get('session') || null;
- this.load();
+ this.tab = params.get('tab') || 'holdings';
+ this.target = params.get('target');
+ this.targetId = params.get('target_id');
+
+ if (this.volcopy.currentContext) {
+ // Avoid clobbering the context on route change.
+ this.context = this.volcopy.currentContext;
+ } else {
+ this.context = new VolCopyContext();
+ this.context.org = this.org; // inject;
+ }
+
+ switch (this.target) {
+ case 'item':
+ this.context.copyId = +this.targetId;
+ break;
+ case 'callnumber':
+ this.context.volId = +this.targetId;
+ break;
+ case 'record':
+ this.context.recordId = +this.targetId;
+ break;
+ case 'session':
+ this.context.session = this.targetId;
+ break;
+ }
+
+ if (!this.volcopy.currentContext) {
+ // Avoid refetching the data during route changes.
+ this.volcopy.currentContext = this.context;
+ this.load();
+ }
}
load(copyIds?: number[]) {
-
this.sessionExpired = false;
this.loading = true;
this.context.reset();
- this.volcopy.fetchDefaults()
- .then(_ => this.volcopy.fetchCopyStats())
+ this.volcopy.load()
.then(_ => this.fetchHoldings(copyIds))
.then(_ => this.volcopy.applyVolLabels(
this.context.volNodes().map(n => n.target)))
- .then(_ => this.holdings.fetchCallNumberClasses())
- .then(_ => this.holdings.fetchCallNumberPrefixes())
- .then(_ => this.holdings.fetchCallNumberSuffixes())
.then(_ => this.context.sortHoldings())
.then(_ => this.context.setRecordId())
.then(_ => this.loading = false);
this.context.sessionType = 'mixed';
return this.fetchSession(this.context.session);
- } else if (this.context.recordId) {
- this.context.sessionType = 'record';
- return this.fetchRecords(this.context.recordId);
+ } else if (this.context.copyId) {
+ this.context.sessionType = 'copy';
+ return this.fetchCopies(this.context.copyId);
} else if (this.context.volId) {
this.context.sessionType = 'vol';
return this.fetchVols(this.context.volId);
- } else if (this.context.copyId) {
- this.context.sessionType = 'copy';
- return this.fetchCopies(this.context.copyId);
+ } else if (this.context.recordId) {
+ this.context.sessionType = 'record';
+ return this.fetchRecords(this.context.recordId);
}
}
+ // Changing a tab in the UI means changing the route.
+ // Changing the route ultimately results in changing the tab.
+ beforeTabChange(evt: NgbTabChangeEvent) {
+ evt.preventDefault();
+ this.tab = evt.nextId;
+ this.routeToTab();
+ }
+
+ routeToTab() {
+ const url =
+ `/staff/cat/volcopy/${this.tab}/${this.target}/${this.targetId}`;
+
+ // Retain search parameters
+ this.router.navigate([url], {queryParamsHandling: 'merge'});
+ }
+
fetchSession(session: string): Promise<any> {
return this.cache.getItem(session, 'edit-these-copies')
volumes.push(vol);
});
+ // De-flesh before posting
+ volumes.forEach(vol => {
+ vol.copies().forEach(copy => {
+ ['editor', 'creator', 'location'].forEach(field => {
+ if (typeof copy[field]() === 'object') {
+ copy[field](copy[field]().id());
+ }
+ });
+ });
+ });
+
if (volumes.length > 0) {
this.saveApi(volumes);
} else {
import {VolCopyContext} from './volcopy';
import {HoldingsService, CallNumData} from '@eg/staff/share/holdings/holdings.service';
import {ServerStoreService} from '@eg/core/server-store.service';
+import {StoreService} from '@eg/core/store.service';
+import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
/* Managing volcopy data */
defaultValues: any = null;
copyStatuses: {[id: number]: IdlObject} = null;
+ // Track this here so it can survive route changes.
+ currentContext: VolCopyContext;
+
+ ageProtectRules: IdlObject[] = [];
+ floatingGroups: IdlObject[] = [];
+ itemTypeMaps: IdlObject[] = [];
+ circModifiers: IdlObject[] = [];
+ statCats: IdlObject[] = [];
+ statCatEntryMap: {[id: number]: IdlObject} = {}; // entry id => entry
+
+ templateNames: ComboboxEntry[] = [];
+ templates: any = {};
+
constructor(
private evt: EventService,
private net: NetService,
private auth: AuthService,
private pcrud: PcrudService,
private holdings: HoldingsService,
- private store: ServerStoreService
+ private store: StoreService,
+ private serverStore: ServerStoreService
) {}
+
+ // Fetch the data that is always needed.
+ load(): Promise<any> {
+
+ if (this.itemTypeMaps.length > 0) {
+ return Promise.resolve();
+ }
+
+ return this.fetchDefaults()
+ .then(_ => this.holdings.fetchCallNumberClasses())
+ .then(_ => this.holdings.fetchCallNumberPrefixes())
+ .then(_ => this.holdings.fetchCallNumberSuffixes())
+ .then(_ => this.fetchCopyStats())
+ .then(_ => this.fetchTemplates())
+ .then(_ => {
+
+ return this.pcrud.retrieveAll('crahp')
+ .pipe(tap(rule => this.ageProtectRules.push(rule))).toPromise()
+
+ }).then(_ => {
+
+ this.ageProtectRules = this.ageProtectRules.sort(
+ (a, b) => a.name() < b.name() ? -1 : 1);
+
+ }).then(_ => {
+
+ return this.pcrud.retrieveAll('cfg')
+ .pipe(tap(rule => this.floatingGroups.push(rule))).toPromise();
+
+ }).then(_ => {
+
+ this.floatingGroups = this.floatingGroups.sort(
+ (a, b) => a.name() < b.name() ? -1 : 1);
+
+ }).then(_ => {
+
+ return this.pcrud.retrieveAll('ccm')
+ .pipe(tap(rule => this.circModifiers.push(rule))).toPromise();
+
+ }).then(_ => {
+
+ this.circModifiers = this.circModifiers.sort(
+ (a, b) => a.name() < b.name() ? -1 : 1);
+
+ }).then(_ => {
+
+ return this.pcrud.retrieveAll('citm')
+ .pipe(tap(itemType => this.itemTypeMaps.push(itemType))).toPromise();
+
+ }).then(_ => {
+
+ this.itemTypeMaps = this.itemTypeMaps.sort(
+ (a, b) => a.value() < b.value() ? -1 : 1);
+
+ }).then(_ => {
+
+ return this.net.request('open-ils.circ',
+ 'open-ils.circ.stat_cat.asset.retrieve.all',
+ this.auth.token(), this.auth.user().ws_ou()
+ ).toPromise().then(stats => this.statCats = stats);
+
+ }).then(_ => {
+
+ // Sort most local to the front of the list.
+ this.statCats = this.statCats.sort((s1, s2) => {
+ const d1 = this.org.get(s1.owner()).ou_type().depth();
+ const d2 = this.org.get(s2.owner()).ou_type().depth();
+
+ if (d1 > d2) {
+ return -1;
+ } else if (d1 < d2) {
+ return 1;
+ } else {
+ return s1.name() < s2.name() ? -1 : 1;
+ }
+ });
+
+ this.statCats.forEach(cat => {
+ cat.entries().forEach(
+ entry => this.statCatEntryMap[entry.id()] = entry);
+ });
+ });
+ }
+
+ fetchTemplates(): Promise<any> {
+
+ // TODO: copy templates should be server settings
+ const tmpls = this.store.getLocalItem('cat.copy.templates');
+ if (!tmpls) { return Promise.resolve(); }
+
+ this.templates = tmpls;
+ this.templateNames = Object.keys(tmpls)
+ .sort((n1, n2) => n1 < n2 ? -1 : 1)
+ .map(name => ({id: name, label: name}));
+
+ return Promise.resolve();
+ }
+
fetchDefaults(): Promise<any> {
if (this.defaultValues) { return Promise.resolve(); }
- return this.store.getItem('cat.copy.defaults').then(
+ return this.serverStore.getItem('cat.copy.defaults').then(
defaults => {
this.defaultValues = defaults || {};
}
copy.ref('f');
copy.mint_condition('t');
copy.parts([]);
+ copy.stat_cat_entries([]);
return copy;
}
return promise;
}
+
+
+
}
o1.target.shortname() < o2.target.shortname() ? -1 : 1);
}
+ // Changes pending and no unresolved issues.
isSaveable(): boolean {
const dupeBc = this.copyList().filter(c => c._dupe_barcode).length;
if (dupeBc) { return false; }
- return true;
+ const modified = (o: IdlObject): boolean => {
+ return o.isnew() || o.ischanged() || o.isdeleted();
+ };
+
+ if (this.volNodes().filter(n => modified(n.target)).length > 0) {
+ return true;
+ }
+
+ if (this.copyList().filter(c => modified(c)).length > 0) {
+ return true;
+ }
+
+ return false;
}
}
// TODO: Support adding like call numbers by getting selected
// call numbers from the holdings grid.
addHoldings() {
- this.holdings.spawnAddHoldingsUi(this.recId);
+ // -1 == create new call number
+ this.holdings.spawnAddHoldingsUi(this.recId, null, [{callnumber: -1}]);
}
}
return this._mode;
}
+ // If true, no attempt is made to save the new alerts to the
+ // database. It's assumed this takes place in the calling code.
+ // This is useful for creating alerts for new copies.
+ @Input() inPlaceMode = false;
+
// In 'create' mode, we may be adding notes to multiple copies.
copies: IdlObject[];
// In 'manage' mode we only handle a single copy.
this.newAlert = this.idl.create('aca');
this.newAlert.create_staff(this.auth.user().id());
- if (this.copyIds.length === 0) {
+ if (this.copyIds.length === 0 && !this.inPlaceMode) {
return throwError('copy ID required');
}
}
getCopies(): Promise<any> {
+
+ if (this.inPlaceMode) { return Promise.resolve(); }
+
return this.pcrud.search('acp', {id: this.copyIds}, {}, {atomic: true})
.toPromise().then(copies => {
this.copies = copies;
addNew() {
if (!this.newAlert.alert_type()) { return; }
+ if (this.inPlaceMode) {
+ this.close(this.newAlert);
+ return;
+ }
+
const alerts: IdlObject[] = [];
this.copies.forEach(c => {
const a = this.idl.clone(this.newAlert);
return;
}
setTimeout(() => {
- const url = `/eg2/staff/cat/volcopy/edit/session/${key}`;
+ const tab = hideVols ? 'attrs' : 'holdings';
+ const url = `/eg2/staff/cat/volcopy/${tab}/session/${key}`;
window.open(url, '_blank');
});
});
color: black;
}
+/* Washed out version of the Bootstrap 'info' background.
+ * Useful for blocking out sections of a page/form without it
+ * being so intensely colorful */
+.bg-faint {
+ /*background-color: rgb(204, 229, 255, 0.3);*/
+
+ /* d9edf7 */
+ /*background-color: rgb(217, 237, 247, 0.5);*/
+
+ background-color: rgba(0,0,0,.03);
+}
+
/* Allow for larger XL dialogs */
@media (min-width: 1300px) { .modal-xl { max-width: 1200px; } }
@media (min-width: 1600px) { .modal-xl { max-width: 1500px; } }