<!-- 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 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">
+ <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"
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 {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',
// Match the header of the batch attrs component
- styles: [`
- .batch-header {background-color: #d9edf7;}`
+ styles: [
+ `.batch-header {background-color: #EBF4FA;}`,
+ `.template-row {background-color: #EBF4FA;}`
]
})
export class CopyAttrsComponent implements OnInit, AfterViewInit {
// Map of stat ID to entry ID.
statCatValues: {[statId: number]: number} = {};
- ageProtectRules: IdlObject[] = [];
- floatingGroups: IdlObject[] = [];
- itemTypeMaps: IdlObject[] = [];
- circModifiers: IdlObject[] = [];
- statCats: IdlObject[] = [];
- statCatEntryMap: {[id: number]: IdlObject} = {}; // entry id => entry
-
loanDurationLabelMap: {[level: number]: string} = {};
fineLevelLabelMap: {[level: number]: string} = {};
@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(_ => {
-
- 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);
- });
- });
+ statCats(): IdlObject[] {
+ return this.volcopy.statCats;
}
+
orgSn(orgId: number): string {
return orgId ? this.org.get(orgId).shortname() : '';
}
let value = '';
if (entry) {
- if (this.statCatEntryMap[entry.id()]) {
- value = this.statCatEntryMap[entry.id()].value();
+ if (this.volcopy.statCatEntryMap[entry.id()]) {
+ value = this.volcopy.statCatEntryMap[entry.id()].value();
} else {
// Map to a remote stat cat. Ignore.
return;
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() : '';
}
entry.id(entryId);
- entry.value(this.statCatEntryMap[entryId].value());
+ entry.value(this.volcopy.statCatEntryMap[entryId].value());
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: #d9edf7;*/
background-color: rgba(0,0,0,.03);
border-top: 1px solid #d9edf7;
border-bottom: 1px solid #d9edf7;
}
-.batch-vol-row {
- border: 2px 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 batch-vol-row mb-2">
+<div class="row d-flex bg-faint mb-2 border border-info 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)}">
<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-info bg-faint">
+ <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>
</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,
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')
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 || {};
}
return promise;
}
+
+
+
}
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; } }