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 {CopyNotesDialogComponent
23 } from '@eg/staff/share/holdings/copy-notes-dialog.component';
24 import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
25 import {BatchItemAttrComponent, BatchChangeSelection
26 } from '@eg/staff/share/holdings/batch-item-attr.component';
27 import {FileExportService} from '@eg/share/util/file-export.service';
30 selector: 'eg-copy-attrs',
31 templateUrl: 'copy-attrs.component.html',
33 // Match the header of the batch attrs component
35 `.batch-header {background-color: #EBF4FA;}`,
36 `.template-row {background-color: #EBF4FA;}`
39 export class CopyAttrsComponent implements OnInit, AfterViewInit {
41 @Input() context: VolCopyContext;
43 // Batch values applied from the form.
44 // Some values are scalar, some IdlObjects depending on copy fleshyness.
45 values: {[field: string]: any} = {};
47 // Map of stat ID to entry ID.
48 statCatValues: {[statId: number]: number} = {};
50 loanDurationLabelMap: {[level: number]: string} = {};
51 fineLevelLabelMap: {[level: number]: string} = {};
53 statCatFilter: number;
55 @ViewChild('loanDurationShort', {static: false})
56 loanDurationShort: StringComponent;
57 @ViewChild('loanDurationNormal', {static: false})
58 loanDurationNormal: StringComponent;
59 @ViewChild('loanDurationLong', {static: false})
60 loanDurationLong: StringComponent;
62 @ViewChild('fineLevelLow', {static: false})
63 fineLevelLow: StringComponent;
64 @ViewChild('fineLevelNormal', {static: false})
65 fineLevelNormal: StringComponent;
66 @ViewChild('fineLevelHigh', {static: false})
67 fineLevelHigh: StringComponent;
69 @ViewChild('mintConditionYes', {static: false})
70 mintConditionYes: StringComponent;
71 @ViewChild('mintConditionNo', {static: false})
72 mintConditionNo: StringComponent;
74 @ViewChild('copyAlertsDialog', {static: false})
75 private copyAlertsDialog: CopyAlertsDialogComponent;
77 @ViewChild('copyTagsDialog', {static: false})
78 private copyTagsDialog: CopyTagsDialogComponent;
80 @ViewChild('copyNotesDialog', {static: false})
81 private copyNotesDialog: CopyNotesDialogComponent;
83 @ViewChild('copyTemplateCbox', {static: false})
84 copyTemplateCbox: ComboboxComponent;
86 @ViewChildren(BatchItemAttrComponent)
87 batchAttrs: QueryList<BatchItemAttrComponent>;
89 // Emitted when the save-ability of this form changes.
90 @Output() canSaveChange: EventEmitter<boolean> = new EventEmitter<boolean>();
93 private router: Router,
94 private route: ActivatedRoute,
95 private evt: EventService,
96 private idl: IdlService,
97 private org: OrgService,
98 private net: NetService,
99 private auth: AuthService,
100 private pcrud: PcrudService,
101 private holdings: HoldingsService,
102 private format: FormatService,
103 private store: StoreService,
104 private fileExport: FileExportService,
105 public volcopy: VolCopyService
109 this.statCatFilter = this.volcopy.defaults.values.statcat_filter;
114 const tmpl = this.store.getLocalItem('cat.copy.last_template');
116 // avoid Express Changed warning w/ timeout
117 setTimeout(() => this.copyTemplateCbox.selectedId = tmpl);
120 this.loanDurationLabelMap[1] = this.loanDurationShort.text;
121 this.loanDurationLabelMap[2] = this.loanDurationNormal.text;
122 this.loanDurationLabelMap[3] = this.loanDurationLong.text;
124 this.fineLevelLabelMap[1] = this.fineLevelLow.text;
125 this.fineLevelLabelMap[2] = this.fineLevelNormal.text;
126 this.fineLevelLabelMap[3] = this.fineLevelHigh.text;
130 statCats(): IdlObject[] {
131 if (this.statCatFilter) {
132 const orgs = this.org.descendants(this.statCatFilter, true);
134 return this.volcopy.commonData.acp_stat_cat.filter(
135 sc => orgs.includes(sc.owner()));
139 return this.volcopy.commonData.acp_stat_cat;
144 orgSn(orgId: number): string {
145 return orgId ? this.org.get(orgId).shortname() : '';
148 statCatCounts(catId: number): {[value: string]: number} {
149 catId = Number(catId);
152 this.context.copyList().forEach(copy => {
153 const entry = copy.stat_cat_entries()
154 .filter(e => e.stat_cat() === catId)[0];
158 if (this.volcopy.statCatEntryMap[entry.id()]) {
159 value = this.volcopy.statCatEntryMap[entry.id()].value();
161 // Map to a remote stat cat. Ignore.
166 if (counts[value] === undefined) {
175 itemAttrCounts(field: string): {[value: string]: number} {
178 this.context.copyList().forEach(copy => {
179 const value = this.getFieldDisplayValue(field, copy);
181 if (counts[value] === undefined) {
190 getFieldDisplayValue(field: string, copy: IdlObject): string {
192 // Some fields don't live directly on the copy.
193 if (field === 'owning_lib') {
195 copy.call_number().owning_lib()).shortname() +
196 ' : ' + copy.call_number().label();
199 const value = copy[field]();
201 if (!value && value !== 0) { return ''; }
206 return this.volcopy.copyStatuses[value].name();
209 return value.name() +
210 ' (' + this.org.get(value.owning_lib()).shortname() + ')';
215 return this.format.transform(
216 {datatype: 'timestamp', value: value});
220 return value.usrname();
223 return this.org.get(value).shortname();
226 const rule = this.volcopy.commonData.acp_age_protect.filter(
227 r => r.id() === Number(value))[0];
228 return rule ? rule.name() : '';
231 const grp = this.volcopy.commonData.acp_floating_group.filter(
232 g => g.id() === Number(value))[0];
233 return grp ? grp.name() : '';
235 case 'loan_duration':
236 return this.loanDurationLabelMap[value];
239 return this.fineLevelLabelMap[value];
242 const map = this.volcopy.commonData.acp_item_type_map.filter(
243 m => m.code() === value)[0];
244 return map ? map.value() : '';
246 case 'circ_modifier':
247 const mod = this.volcopy.commonData.acp_circ_modifier.filter(
248 m => m.code() === value)[0];
249 return mod ? mod.name() : '';
251 case 'mint_condition':
252 if (!this.mintConditionYes) { return ''; }
253 return value === 't' ?
254 this.mintConditionYes.text : this.mintConditionNo.text;
260 copyWantsChange(copy: IdlObject, field: string,
261 changeSelection: BatchChangeSelection): boolean {
262 const disValue = this.getFieldDisplayValue(field, copy);
263 return changeSelection[disValue] === true;
266 applyCopyValue(field: string, value?: any, changeSelection?: BatchChangeSelection) {
267 if (value === undefined) {
268 value = this.values[field];
270 this.values[field] = value;
273 if (field === 'owning_lib') {
274 this.owningLibChanged(value, changeSelection);
278 this.context.copyList().forEach(copy => {
279 if (!copy[field] || copy[field]() === value) { return; }
281 // Change selection indicates which items should be modified
282 // based on the display value for the selected field at
284 if (changeSelection &&
285 !this.copyWantsChange(copy, field, changeSelection)) {
290 copy.ischanged(true);
294 this.emitSaveChange();
297 owningLibChanged(orgId: number, changeSelection?: BatchChangeSelection) {
298 if (!orgId) { return; }
300 // Map existing vol IDs to their replacments.
301 const newVols: any = {};
303 this.context.copyList().forEach(copy => {
305 if (changeSelection &&
306 !this.copyWantsChange(copy, 'owning_lib', changeSelection)) {
310 // Change the copy circ lib to match the new owning lib
311 // if configured to do so.
312 if (this.volcopy.defaults.values.circ_lib_mod_with_owning_lib) {
313 if (copy.circ_lib() !== orgId) {
314 copy.circ_lib(orgId);
315 copy.ischanged(true);
318 .filter(ba => ba.name === 'circ_lib')
319 .forEach(attr => attr.hasChanged = true);
323 const vol = copy.call_number();
325 if (vol.owning_lib() === orgId) { return; } // No change needed
328 if (newVols[vol.id()]) {
329 newVol = newVols[vol.id()];
333 // The open-ils.cat.asset.volume.fleshed.batch.update API
334 // will use the existing volume when trying to create a
335 // new volume with the same parameters as an existing volume.
336 newVol = this.idl.clone(vol);
337 newVol.owning_lib(orgId);
338 newVol.id(this.volcopy.autoId--);
340 newVols[vol.id()] = newVol;
343 copy.call_number(newVol);
344 copy.ischanged(true);
346 this.context.removeCopyNode(copy.id());
347 this.context.findOrCreateCopyNode(copy);
350 // If any of the above actions results in an empty volume
351 // remove it from the tree. Note this does not delete the
352 // volume at the server, since other items could be attached
353 // of which this instance of the editor is not aware.
354 Object.keys(newVols).forEach(volId => {
356 const volNode = this.context.volNodes().filter(
357 node => node.target.id() === +volId)[0];
359 if (volNode && volNode.children.length === 0) {
360 this.context.removeVolNode(+volId);
365 // Create or modify a stat cat entry for each copy that does not
366 // already match the new value.
367 statCatChanged(catId: number, clear?: boolean) {
368 catId = Number(catId);
370 const entryId = this.statCatValues[catId];
372 if (!clear && (!entryId || !this.volcopy.statCatEntryMap[entryId])) {
374 `Attempt to apply stat cat value which does not exist.
375 This is likely the result of a stale copy template.
376 stat_cat=${catId} entry=${entryId}`);
381 this.context.copyList().forEach(copy => {
383 let entry = copy.stat_cat_entries()
384 .filter(e => e.stat_cat() === catId)[0];
389 // Removing the entry map (and setting copy.ishanged) is
390 // enough to tell the API to delete it.
392 copy.stat_cat_entries(copy.stat_cat_entries()
393 .filter(e => e.stat_cat() !== catId));
399 if (entry.id() === entryId) {
400 // Requested mapping already exists.
405 // Copy has no entry for this stat cat yet.
406 entry = this.idl.create('asce');
407 entry.stat_cat(catId);
408 copy.stat_cat_entries().push(entry);
412 entry.value(this.volcopy.statCatEntryMap[entryId].value());
415 copy.ischanged(true);
418 this.emitSaveChange();
422 this.copyAlertsDialog.inPlaceCreateMode = true;
423 this.copyAlertsDialog.copyIds = this.context.copyList().map(c => c.id());
425 this.copyAlertsDialog.open({size: 'lg'}).subscribe(
428 this.context.copyList().forEach(copy => {
429 const a = this.idl.clone(newAlert);
432 if (!copy.copy_alerts()) { copy.copy_alerts([]); }
433 copy.copy_alerts().push(a);
434 copy.ischanged(true);
442 this.copyTagsDialog.inPlaceCreateMode = true;
443 this.copyTagsDialog.copyIds = this.context.copyList().map(c => c.id());
445 this.copyTagsDialog.open({size: 'lg'}).subscribe(changes => {
446 if ((!changes.newTags || changes.newTags.length === 0) &&
447 (!changes.deletedMaps || changes.deletedMaps.length === 0)) {
451 changes.newTags.forEach(tag => {
452 this.context.copyList().forEach(copy => {
454 if (copy.tags().filter(
455 m => m.tag() === tag.id()).length > 0) {
456 return; // map already exists
459 const map = this.idl.create('acptcm');
464 copy.tags().push(map);
465 copy.ischanged(true);
469 if (this.context.copyList().length === 1) {
470 const copy = this.context.copyList()[0];
471 changes.deletedMaps.forEach(tag => {
472 const existing = copy.tags().filter(t => t.id() === tag.id())[0];
474 existing.isdeleted(true);
475 copy.ischanged(true);
483 this.copyNotesDialog.inPlaceCreateMode = true;
484 this.copyNotesDialog.copyIds = this.context.copyList().map(c => c.id());
486 this.copyNotesDialog.open({size: 'lg'}).subscribe(changes => {
487 if (!changes) { return; }
489 if ((!changes.newNotes || changes.newNotes.length === 0) &&
490 (!changes.delNotes || changes.delNotes.length === 0)
495 changes.newNotes.forEach(note => {
496 this.context.copyList().forEach(copy => {
497 const n = this.idl.clone(note);
498 n.owning_copy(copy.id());
499 copy.notes().push(n);
500 copy.ischanged(true);
503 if (this.context.copyList().length === 1) {
504 const copy = this.context.copyList()[0];
505 changes.delNotes.forEach(note => {
506 const existing = copy.notes().filter(n => n.id() === note.id())[0];
508 existing.isdeleted(true);
509 copy.ischanged(true);
517 const entry = this.copyTemplateCbox.selected;
518 if (!entry) { return; }
520 this.store.setLocalItem('cat.copy.last_template', entry.id);
522 const template = this.volcopy.templates[entry.id];
524 Object.keys(template).forEach(field => {
525 const value = template[field];
527 if (value === null || value === undefined) { return; }
529 if (field === 'statcats') {
530 Object.keys(value).forEach(catId => {
531 this.statCatValues[+catId] = value[+catId];
532 this.statCatChanged(+catId);
537 // In some cases, we may have to fetch the data since
538 // the local code assumes copy field is fleshed.
539 let promise = Promise.resolve(value);
541 if (field === 'location') {
542 // May be a 'remote' location. Fetch as needed.
543 promise = this.volcopy.getLocation(value);
546 promise.then(val => {
547 this.applyCopyValue(field, val);
549 // Indicate in the form these values have changed
551 .filter(ba => ba.name === field)
552 .forEach(attr => attr.hasChanged = true);
558 const entry: ComboboxEntry = this.copyTemplateCbox.selected;
559 if (!entry) { return; }
564 if (entry.freetext) {
566 // freetext entries don't have an ID, but we may need one later.
567 entry.id = entry.label;
571 template = this.volcopy.templates[name];
574 this.batchAttrs.forEach(comp => {
575 if (!comp.hasChanged) { return; }
577 const field = comp.name;
578 const value = this.values[field];
580 if (value === null) {
581 delete template[field];
585 if (field.match(/stat_cat_/)) {
586 const statId = field.match(/stat_cat_(\d+)/)[1];
587 if (!template.statcats) { template.statcats = {}; }
589 template.statcats[statId] = value;
593 // Some values are fleshed. this assumes fleshed objects
594 // have an 'id' value, which is true so far.
596 typeof value === 'object' ? value.id() : value;
600 this.volcopy.templates[name] = template;
601 this.volcopy.saveTemplates();
604 exportTemplate($event) {
605 if (this.fileExport.inProgress()) { return; }
607 this.fileExport.exportFile(
608 $event, JSON.stringify(this.volcopy.templates), 'text/json');
611 importTemplate($event) {
612 const file: File = $event.target.files[0];
613 if (!file) { return; }
615 const reader = new FileReader();
617 reader.addEventListener('load', () => {
620 const template = JSON.parse(reader.result as string);
621 const name = Object.keys(template)[0];
622 this.volcopy.templates[name] = template[name];
624 console.error('Invalid Item Attribute template', E);
628 this.volcopy.saveTemplates();
629 // Adds the new one to the list and re-sorts the labels.
630 this.volcopy.fetchTemplates();
633 reader.readAsText(file);
636 // Returns null when no export is in progress.
637 exportTemplateUrl(): SafeUrl {
638 return this.fileExport.safeUrl;
642 const entry: ComboboxEntry = this.copyTemplateCbox.selected;
643 if (!entry) { return; }
644 delete this.volcopy.templates[entry.id];
645 this.volcopy.saveTemplates();
646 this.copyTemplateCbox.selected = null;
649 displayAttr(field: string): boolean {
650 return this.volcopy.defaults.hidden[field] !== true;
653 copyFieldLabel(field: string): string {
654 const def = this.idl.classes.acp.field_map[field];
655 return def ? def.label : '';
658 // Returns false if any items are in magic statuses
659 statusEditable(): boolean {
660 const copies = this.context.copyList();
661 for (let idx = 0; idx < copies.length; idx++) {
662 if (this.volcopy.copyStatIsMagic(copies[idx].status())) {
669 // Called any time a change occurs that could affect the
670 // save-ability of the form.
673 const canSave = this.batchAttrs.filter(
674 attr => attr.warnOnRequired()).length === 0;
676 this.canSaveChange.emit(canSave);
680 // True if one of our batch editors has been put into edit
681 // mode and left there without an Apply, Cancel, or Clear
682 hasActiveInput(): boolean {
683 return this.batchAttrs.filter(attr => attr.editing).length > 0;
686 applyPendingChanges() {
687 // If a user has left any changes in the 'editing' state, this
688 // will go through and apply the values so they do not have to
689 // click Apply for every one.
690 this.batchAttrs.filter(attr => attr.editing).forEach(attr => attr.save());
693 affectedOrgIds(): number[] {
694 if (!this.context) { return []; }
695 return this.context.orgNodes().map(n => n.target.id());