1 import {Component, Input, OnInit, AfterViewInit, ViewChild,
2 EventEmitter, Output, QueryList, ViewChildren} from '@angular/core';
3 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
4 import {SafeUrl} from '@angular/platform-browser';
5 import {tap} from 'rxjs/operators';
6 import {IdlObject, IdlService} from '@eg/core/idl.service';
7 import {EventService} from '@eg/core/event.service';
8 import {OrgService} from '@eg/core/org.service';
9 import {StoreService} from '@eg/core/store.service';
10 import {NetService} from '@eg/core/net.service';
11 import {AuthService} from '@eg/core/auth.service';
12 import {PcrudService} from '@eg/core/pcrud.service';
13 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
14 import {VolCopyContext} from './volcopy';
15 import {VolCopyService} from './volcopy.service';
16 import {FormatService} from '@eg/core/format.service';
17 import {StringComponent} from '@eg/share/string/string.component';
18 import {CopyAlertsDialogComponent
19 } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
20 import {CopyTagsDialogComponent
21 } from '@eg/staff/share/holdings/copy-tags-dialog.component';
22 import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
23 import {BatchItemAttrComponent, BatchChangeSelection
24 } from '@eg/staff/share/holdings/batch-item-attr.component';
25 import {FileExportService} from '@eg/share/util/file-export.service';
28 selector: 'eg-copy-attrs',
29 templateUrl: 'copy-attrs.component.html',
31 // Match the header of the batch attrs component
33 `.batch-header {background-color: #EBF4FA;}`,
34 `.template-row {background-color: #EBF4FA;}`
37 export class CopyAttrsComponent implements OnInit, AfterViewInit {
39 @Input() context: VolCopyContext;
41 // Batch values applied from the form.
42 // Some values are scalar, some IdlObjects depending on copy fleshyness.
43 values: {[field: string]: any} = {};
45 // Map of stat ID to entry ID.
46 statCatValues: {[statId: number]: number} = {};
48 loanDurationLabelMap: {[level: number]: string} = {};
49 fineLevelLabelMap: {[level: number]: string} = {};
51 statCatFilter: number;
53 @ViewChild('loanDurationShort', {static: false})
54 loanDurationShort: StringComponent;
55 @ViewChild('loanDurationNormal', {static: false})
56 loanDurationNormal: StringComponent;
57 @ViewChild('loanDurationLong', {static: false})
58 loanDurationLong: StringComponent;
60 @ViewChild('fineLevelLow', {static: false})
61 fineLevelLow: StringComponent;
62 @ViewChild('fineLevelNormal', {static: false})
63 fineLevelNormal: StringComponent;
64 @ViewChild('fineLevelHigh', {static: false})
65 fineLevelHigh: StringComponent;
67 @ViewChild('mintConditionYes', {static: false})
68 mintConditionYes: StringComponent;
69 @ViewChild('mintConditionNo', {static: false})
70 mintConditionNo: StringComponent;
72 @ViewChild('copyAlertsDialog', {static: false})
73 private copyAlertsDialog: CopyAlertsDialogComponent;
75 @ViewChild('copyTagsDialog', {static: false})
76 private copyTagsDialog: CopyTagsDialogComponent;
78 @ViewChild('copyTemplateCbox', {static: false})
79 copyTemplateCbox: ComboboxComponent;
81 @ViewChildren(BatchItemAttrComponent)
82 batchAttrs: QueryList<BatchItemAttrComponent>;
84 // Emitted when the save-ability of this form changes.
85 @Output() canSaveChange: EventEmitter<boolean> = new EventEmitter<boolean>();
88 private router: Router,
89 private route: ActivatedRoute,
90 private evt: EventService,
91 private idl: IdlService,
92 private org: OrgService,
93 private net: NetService,
94 private auth: AuthService,
95 private pcrud: PcrudService,
96 private holdings: HoldingsService,
97 private format: FormatService,
98 private store: StoreService,
99 private fileExport: FileExportService,
100 public volcopy: VolCopyService
104 this.statCatFilter = this.volcopy.defaults.values.statcat_filter;
109 const tmpl = this.store.getLocalItem('cat.copy.last_template');
111 // avoid Express Changed warning w/ timeout
112 setTimeout(() => this.copyTemplateCbox.selectedId = tmpl);
115 this.loanDurationLabelMap[1] = this.loanDurationShort.text;
116 this.loanDurationLabelMap[2] = this.loanDurationNormal.text;
117 this.loanDurationLabelMap[3] = this.loanDurationLong.text;
119 this.fineLevelLabelMap[1] = this.fineLevelLow.text;
120 this.fineLevelLabelMap[2] = this.fineLevelNormal.text;
121 this.fineLevelLabelMap[3] = this.fineLevelHigh.text;
125 statCats(): IdlObject[] {
126 if (this.statCatFilter) {
127 const orgs = this.org.descendants(this.statCatFilter, true);
129 return this.volcopy.commonData.acp_stat_cat.filter(
130 sc => orgs.includes(sc.owner()));
134 return this.volcopy.commonData.acp_stat_cat;
139 orgSn(orgId: number): string {
140 return orgId ? this.org.get(orgId).shortname() : '';
143 statCatCounts(catId: number): {[value: string]: number} {
144 catId = Number(catId);
147 this.context.copyList().forEach(copy => {
148 const entry = copy.stat_cat_entries()
149 .filter(e => e.stat_cat() === catId)[0];
153 if (this.volcopy.statCatEntryMap[entry.id()]) {
154 value = this.volcopy.statCatEntryMap[entry.id()].value();
156 // Map to a remote stat cat. Ignore.
161 if (counts[value] === undefined) {
170 itemAttrCounts(field: string): {[value: string]: number} {
173 this.context.copyList().forEach(copy => {
174 const value = this.getFieldDisplayValue(field, copy);
176 if (counts[value] === undefined) {
185 getFieldDisplayValue(field: string, copy: IdlObject): string {
187 // Some fields don't live directly on the copy.
188 if (field === 'owning_lib') {
190 copy.call_number().owning_lib()).shortname() +
191 ' : ' + copy.call_number().label();
194 const value = copy[field]();
196 if (!value && value !== 0) { return ''; }
201 return this.volcopy.copyStatuses[value].name();
204 return value.name() +
205 ' (' + this.org.get(value.owning_lib()).shortname() + ')';
210 return this.format.transform(
211 {datatype: 'timestamp', value: value});
215 return value.usrname();
218 return this.org.get(value).shortname();
221 const rule = this.volcopy.commonData.acp_age_protect.filter(
222 r => r.id() === Number(value))[0];
223 return rule ? rule.name() : '';
226 const grp = this.volcopy.commonData.acp_floating_group.filter(
227 g => g.id() === Number(value))[0];
228 return grp ? grp.name() : '';
230 case 'loan_duration':
231 return this.loanDurationLabelMap[value];
234 return this.fineLevelLabelMap[value];
237 const map = this.volcopy.commonData.acp_item_type_map.filter(
238 m => m.code() === value)[0];
239 return map ? map.value() : '';
241 case 'circ_modifier':
242 const mod = this.volcopy.commonData.acp_circ_modifier.filter(
243 m => m.code() === value)[0];
244 return mod ? mod.name() : '';
246 case 'mint_condition':
247 if (!this.mintConditionYes) { return ''; }
248 return value === 't' ?
249 this.mintConditionYes.text : this.mintConditionNo.text;
255 copyWantsChange(copy: IdlObject, field: string,
256 changeSelection: BatchChangeSelection): boolean {
257 const disValue = this.getFieldDisplayValue(field, copy);
258 return changeSelection[disValue] === true;
261 applyCopyValue(field: string, value?: any, changeSelection?: BatchChangeSelection) {
262 if (value === undefined) {
263 value = this.values[field];
265 this.values[field] = value;
268 if (field === 'owning_lib') {
269 this.owningLibChanged(value, changeSelection);
273 this.context.copyList().forEach(copy => {
274 if (!copy[field] || copy[field]() === value) { return; }
276 // Change selection indicates which items should be modified
277 // based on the display value for the selected field at
279 if (changeSelection &&
280 !this.copyWantsChange(copy, field, changeSelection)) {
285 copy.ischanged(true);
289 this.emitSaveChange();
292 owningLibChanged(orgId: number, changeSelection?: BatchChangeSelection) {
293 if (!orgId) { return; }
295 // Map existing vol IDs to their replacments.
296 const newVols: any = {};
298 this.context.copyList().forEach(copy => {
300 if (changeSelection &&
301 !this.copyWantsChange(copy, 'owning_lib', changeSelection)) {
305 // Change the copy circ lib to match the new owning lib
306 // if configured to do so.
307 if (this.volcopy.defaults.values.circ_lib_mod_with_owning_lib) {
308 if (copy.circ_lib() !== orgId) {
309 copy.circ_lib(orgId);
310 copy.ischanged(true);
313 .filter(ba => ba.name === 'circ_lib')
314 .forEach(attr => attr.hasChanged = true);
318 const vol = copy.call_number();
320 if (vol.owning_lib() === orgId) { return; } // No change needed
323 if (newVols[vol.id()]) {
324 newVol = newVols[vol.id()];
328 // The open-ils.cat.asset.volume.fleshed.batch.update API
329 // will use the existing volume when trying to create a
330 // new volume with the same parameters as an existing volume.
331 newVol = this.idl.clone(vol);
332 newVol.owning_lib(orgId);
333 newVol.id(this.volcopy.autoId--);
335 newVols[vol.id()] = newVol;
338 copy.call_number(newVol);
341 this.context.removeCopyNode(copy.id());
342 this.context.findOrCreateCopyNode(copy);
345 // If any of the above actions results in an empty volume
346 // remove it from the tree. Note this does not delete the
347 // volume at the server, since other items could be attached
348 // of which this instance of the editor is not aware.
349 Object.keys(newVols).forEach(volId => {
351 const volNode = this.context.volNodes().filter(
352 node => node.target.id() === +volId)[0];
354 if (volNode && volNode.children.length === 0) {
355 this.context.removeVolNode(+volId);
360 // Create or modify a stat cat entry for each copy that does not
361 // already match the new value.
362 statCatChanged(catId: number, clear?: boolean) {
363 catId = Number(catId);
365 const entryId = this.statCatValues[catId];
367 if (!entryId || !this.volcopy.statCatEntryMap[entryId]) {
369 `Attempt to apply stat cat value which does not exist.
370 This is likely the result of a stale copy template.
371 stat_cat=${catId} entry=${entryId}`);
376 this.context.copyList().forEach(copy => {
378 let entry = copy.stat_cat_entries()
379 .filter(e => e.stat_cat() === catId)[0];
382 if (entry.id() === entryId) {
383 // Requested mapping already exists.
388 // Copy has no entry for this stat cat yet.
389 entry = this.idl.create('asce');
390 entry.stat_cat(catId);
391 copy.stat_cat_entries().push(entry);
395 entry.value(this.volcopy.statCatEntryMap[entryId].value());
397 copy.ischanged(true);
402 this.copyAlertsDialog.inPlaceMode = true;
403 this.copyAlertsDialog.copyIds = this.context.copyList().map(c => c.id());
405 this.copyAlertsDialog.open({size: 'lg'}).subscribe(
408 this.context.copyList().forEach(copy => {
409 const a = this.idl.clone(newAlert);
412 if (!copy.copy_alerts()) { copy.copy_alerts([]); }
413 copy.copy_alerts().push(a);
414 copy.ischanged(true);
422 this.copyTagsDialog.inPlaceMode = true;
423 this.copyTagsDialog.copyIds = this.context.copyList().map(c => c.id());
425 this.copyTagsDialog.open({size: 'lg'}).subscribe(newTags => {
426 if (!newTags || newTags.length === 0) { return; }
428 newTags.forEach(tag => {
429 this.context.copyList().forEach(copy => {
431 if (copy.tags().filter(
432 m => m.tag().id() === tag.id()).length > 0) {
433 return; // map already exists
436 const map = this.idl.create('acptcm');
441 copy.tags().push(map);
442 copy.ischanged(true);
449 const entry = this.copyTemplateCbox.selected;
450 if (!entry) { return; }
452 this.store.setLocalItem('cat.copy.last_template', entry.id);
454 const template = this.volcopy.templates[entry.id];
456 Object.keys(template).forEach(field => {
457 const value = template[field];
459 if (value === null || value === undefined) { return; }
461 if (field === 'statcats') {
462 Object.keys(value).forEach(catId => {
463 this.statCatValues[+catId] = value[+catId];
464 this.statCatChanged(+catId);
469 // In some cases, we may have to fetch the data since
470 // the local code assumes copy field is fleshed.
471 let promise = Promise.resolve(value);
473 if (field === 'location') {
474 // May be a 'remote' location. Fetch as needed.
475 promise = this.volcopy.getLocation(value);
478 promise.then(val => {
479 this.applyCopyValue(field, val);
481 // Indicate in the form these values have changed
483 .filter(ba => ba.name === field)
484 .forEach(attr => attr.hasChanged = true);
490 const entry: ComboboxEntry = this.copyTemplateCbox.selected;
491 if (!entry) { return; }
496 if (entry.freetext) {
498 // freetext entries don't have an ID, but we may need one later.
499 entry.id = entry.label;
503 template = this.volcopy.templates[name];
506 this.batchAttrs.forEach(comp => {
507 if (!comp.hasChanged) { return; }
509 const field = comp.name;
510 const value = this.values[field];
512 if (value === null) {
513 delete template[field];
517 if (field.match(/stat_cat_/)) {
518 const statId = field.match(/stat_cat_(\d+)/)[1];
519 if (!template.statcats) { template.statcats = {}; }
521 template.statcats[statId] = value;
525 // Some values are fleshed. this assumes fleshed objects
526 // have an 'id' value, which is true so far.
528 typeof value === 'object' ? value.id() : value;
532 this.volcopy.templates[name] = template;
533 this.volcopy.saveTemplates();
536 exportTemplate($event) {
537 if (this.fileExport.inProgress()) { return; }
539 this.fileExport.exportFile(
540 $event, JSON.stringify(this.volcopy.templates), 'text/json');
543 importTemplate($event) {
544 const file: File = $event.target.files[0];
545 if (!file) { return; }
547 const reader = new FileReader();
549 reader.addEventListener('load', () => {
552 const template = JSON.parse(reader.result as string);
553 const name = Object.keys(template)[0];
554 this.volcopy.templates[name] = template[name];
556 console.error('Invalid Item Attribute template', E);
560 this.volcopy.saveTemplates();
561 // Adds the new one to the list and re-sorts the labels.
562 this.volcopy.fetchTemplates();
565 reader.readAsText(file);
568 // Returns null when no export is in progress.
569 exportTemplateUrl(): SafeUrl {
570 return this.fileExport.safeUrl;
574 const entry: ComboboxEntry = this.copyTemplateCbox.selected;
575 if (!entry) { return; }
576 delete this.volcopy.templates[entry.id];
577 this.volcopy.saveTemplates();
578 this.copyTemplateCbox.selected = null;
581 displayAttr(field: string): boolean {
582 return this.volcopy.defaults.hidden[field] !== true;
585 copyFieldLabel(field: string): string {
586 const def = this.idl.classes.acp.field_map[field];
587 return def ? def.label : '';
590 // Returns false if any items are in magic statuses
591 statusEditable(): boolean {
592 const copies = this.context.copyList();
593 for (let idx = 0; idx < copies.length; idx++) {
594 if (this.volcopy.copyStatIsMagic(copies[idx].status())) {
601 // Called any time a change occurs that could affect the
602 // save-ability of the form.
605 const canSave = this.batchAttrs.filter(
606 attr => attr.warnOnRequired()).length === 0;
608 this.canSaveChange.emit(canSave);