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);
420 this.copyAlertsDialog.inPlaceCreateMode = true;
421 this.copyAlertsDialog.copyIds = this.context.copyList().map(c => c.id());
423 this.copyAlertsDialog.open({size: 'lg'}).subscribe(
426 this.context.copyList().forEach(copy => {
427 const a = this.idl.clone(newAlert);
430 if (!copy.copy_alerts()) { copy.copy_alerts([]); }
431 copy.copy_alerts().push(a);
432 copy.ischanged(true);
440 this.copyTagsDialog.inPlaceCreateMode = true;
441 this.copyTagsDialog.copyIds = this.context.copyList().map(c => c.id());
443 this.copyTagsDialog.open({size: 'lg'}).subscribe(newTags => {
444 if (!newTags || newTags.length === 0) { return; }
446 newTags.forEach(tag => {
447 this.context.copyList().forEach(copy => {
449 if (copy.tags().filter(
450 m => m.tag().id() === tag.id()).length > 0) {
451 return; // map already exists
454 const map = this.idl.create('acptcm');
459 copy.tags().push(map);
460 copy.ischanged(true);
467 this.copyNotesDialog.inPlaceCreateMode = true;
468 this.copyNotesDialog.copyIds = this.context.copyList().map(c => c.id());
470 this.copyNotesDialog.open({size: 'lg'}).subscribe(newNotes => {
471 if (!newNotes || newNotes.length === 0) { return; }
473 console.log(newNotes);
474 newNotes.forEach(note => {
475 this.context.copyList().forEach(copy => {
476 const n = this.idl.clone(note);
477 n.owning_copy(copy.id());
478 copy.notes().push(n);
479 copy.ischanged(true);
486 const entry = this.copyTemplateCbox.selected;
487 if (!entry) { return; }
489 this.store.setLocalItem('cat.copy.last_template', entry.id);
491 const template = this.volcopy.templates[entry.id];
493 Object.keys(template).forEach(field => {
494 const value = template[field];
496 if (value === null || value === undefined) { return; }
498 if (field === 'statcats') {
499 Object.keys(value).forEach(catId => {
500 this.statCatValues[+catId] = value[+catId];
501 this.statCatChanged(+catId);
506 // In some cases, we may have to fetch the data since
507 // the local code assumes copy field is fleshed.
508 let promise = Promise.resolve(value);
510 if (field === 'location') {
511 // May be a 'remote' location. Fetch as needed.
512 promise = this.volcopy.getLocation(value);
515 promise.then(val => {
516 this.applyCopyValue(field, val);
518 // Indicate in the form these values have changed
520 .filter(ba => ba.name === field)
521 .forEach(attr => attr.hasChanged = true);
527 const entry: ComboboxEntry = this.copyTemplateCbox.selected;
528 if (!entry) { return; }
533 if (entry.freetext) {
535 // freetext entries don't have an ID, but we may need one later.
536 entry.id = entry.label;
540 template = this.volcopy.templates[name];
543 this.batchAttrs.forEach(comp => {
544 if (!comp.hasChanged) { return; }
546 const field = comp.name;
547 const value = this.values[field];
549 if (value === null) {
550 delete template[field];
554 if (field.match(/stat_cat_/)) {
555 const statId = field.match(/stat_cat_(\d+)/)[1];
556 if (!template.statcats) { template.statcats = {}; }
558 template.statcats[statId] = value;
562 // Some values are fleshed. this assumes fleshed objects
563 // have an 'id' value, which is true so far.
565 typeof value === 'object' ? value.id() : value;
569 this.volcopy.templates[name] = template;
570 this.volcopy.saveTemplates();
573 exportTemplate($event) {
574 if (this.fileExport.inProgress()) { return; }
576 this.fileExport.exportFile(
577 $event, JSON.stringify(this.volcopy.templates), 'text/json');
580 importTemplate($event) {
581 const file: File = $event.target.files[0];
582 if (!file) { return; }
584 const reader = new FileReader();
586 reader.addEventListener('load', () => {
589 const template = JSON.parse(reader.result as string);
590 const name = Object.keys(template)[0];
591 this.volcopy.templates[name] = template[name];
593 console.error('Invalid Item Attribute template', E);
597 this.volcopy.saveTemplates();
598 // Adds the new one to the list and re-sorts the labels.
599 this.volcopy.fetchTemplates();
602 reader.readAsText(file);
605 // Returns null when no export is in progress.
606 exportTemplateUrl(): SafeUrl {
607 return this.fileExport.safeUrl;
611 const entry: ComboboxEntry = this.copyTemplateCbox.selected;
612 if (!entry) { return; }
613 delete this.volcopy.templates[entry.id];
614 this.volcopy.saveTemplates();
615 this.copyTemplateCbox.selected = null;
618 displayAttr(field: string): boolean {
619 return this.volcopy.defaults.hidden[field] !== true;
622 copyFieldLabel(field: string): string {
623 const def = this.idl.classes.acp.field_map[field];
624 return def ? def.label : '';
627 // Returns false if any items are in magic statuses
628 statusEditable(): boolean {
629 const copies = this.context.copyList();
630 for (let idx = 0; idx < copies.length; idx++) {
631 if (this.volcopy.copyStatIsMagic(copies[idx].status())) {
638 // Called any time a change occurs that could affect the
639 // save-ability of the form.
642 const canSave = this.batchAttrs.filter(
643 attr => attr.warnOnRequired()).length === 0;
645 this.canSaveChange.emit(canSave);
649 // True if one of our batch editors has been put into edit
650 // mode and left there without an Apply, Cancel, or Clear
651 hasActiveInput(): boolean {
652 return this.batchAttrs.filter(attr => attr.editing).length > 0;
655 applyPendingChanges() {
656 // If a user has left any changes in the 'editing' state, this
657 // will go through and apply the values so they do not have to
658 // click Apply for every one.
659 this.batchAttrs.filter(attr => attr.editing).forEach(attr => attr.save());
662 affectedOrgIds(): number[] {
663 if (!this.context) { return []; }
664 return this.context.orgNodes().map(n => n.target.id());