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 (!clear && (!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];
384 // Removing the entry map (and setting copy.ishanged) is
385 // enough to tell the API to delete it.
387 copy.stat_cat_entries(copy.stat_cat_entries()
388 .filter(e => e.stat_cat() !== catId));
394 if (entry.id() === entryId) {
395 // Requested mapping already exists.
400 // Copy has no entry for this stat cat yet.
401 entry = this.idl.create('asce');
402 entry.stat_cat(catId);
403 copy.stat_cat_entries().push(entry);
407 entry.value(this.volcopy.statCatEntryMap[entryId].value());
410 copy.ischanged(true);
415 this.copyAlertsDialog.inPlaceMode = true;
416 this.copyAlertsDialog.copyIds = this.context.copyList().map(c => c.id());
418 this.copyAlertsDialog.open({size: 'lg'}).subscribe(
421 this.context.copyList().forEach(copy => {
422 const a = this.idl.clone(newAlert);
425 if (!copy.copy_alerts()) { copy.copy_alerts([]); }
426 copy.copy_alerts().push(a);
427 copy.ischanged(true);
435 this.copyTagsDialog.inPlaceMode = true;
436 this.copyTagsDialog.copyIds = this.context.copyList().map(c => c.id());
438 this.copyTagsDialog.open({size: 'lg'}).subscribe(newTags => {
439 if (!newTags || newTags.length === 0) { return; }
441 newTags.forEach(tag => {
442 this.context.copyList().forEach(copy => {
444 if (copy.tags().filter(
445 m => m.tag().id() === tag.id()).length > 0) {
446 return; // map already exists
449 const map = this.idl.create('acptcm');
454 copy.tags().push(map);
455 copy.ischanged(true);
462 const entry = this.copyTemplateCbox.selected;
463 if (!entry) { return; }
465 this.store.setLocalItem('cat.copy.last_template', entry.id);
467 const template = this.volcopy.templates[entry.id];
469 Object.keys(template).forEach(field => {
470 const value = template[field];
472 if (value === null || value === undefined) { return; }
474 if (field === 'statcats') {
475 Object.keys(value).forEach(catId => {
476 this.statCatValues[+catId] = value[+catId];
477 this.statCatChanged(+catId);
482 // In some cases, we may have to fetch the data since
483 // the local code assumes copy field is fleshed.
484 let promise = Promise.resolve(value);
486 if (field === 'location') {
487 // May be a 'remote' location. Fetch as needed.
488 promise = this.volcopy.getLocation(value);
491 promise.then(val => {
492 this.applyCopyValue(field, val);
494 // Indicate in the form these values have changed
496 .filter(ba => ba.name === field)
497 .forEach(attr => attr.hasChanged = true);
503 const entry: ComboboxEntry = this.copyTemplateCbox.selected;
504 if (!entry) { return; }
509 if (entry.freetext) {
511 // freetext entries don't have an ID, but we may need one later.
512 entry.id = entry.label;
516 template = this.volcopy.templates[name];
519 this.batchAttrs.forEach(comp => {
520 if (!comp.hasChanged) { return; }
522 const field = comp.name;
523 const value = this.values[field];
525 if (value === null) {
526 delete template[field];
530 if (field.match(/stat_cat_/)) {
531 const statId = field.match(/stat_cat_(\d+)/)[1];
532 if (!template.statcats) { template.statcats = {}; }
534 template.statcats[statId] = value;
538 // Some values are fleshed. this assumes fleshed objects
539 // have an 'id' value, which is true so far.
541 typeof value === 'object' ? value.id() : value;
545 this.volcopy.templates[name] = template;
546 this.volcopy.saveTemplates();
549 exportTemplate($event) {
550 if (this.fileExport.inProgress()) { return; }
552 this.fileExport.exportFile(
553 $event, JSON.stringify(this.volcopy.templates), 'text/json');
556 importTemplate($event) {
557 const file: File = $event.target.files[0];
558 if (!file) { return; }
560 const reader = new FileReader();
562 reader.addEventListener('load', () => {
565 const template = JSON.parse(reader.result as string);
566 const name = Object.keys(template)[0];
567 this.volcopy.templates[name] = template[name];
569 console.error('Invalid Item Attribute template', E);
573 this.volcopy.saveTemplates();
574 // Adds the new one to the list and re-sorts the labels.
575 this.volcopy.fetchTemplates();
578 reader.readAsText(file);
581 // Returns null when no export is in progress.
582 exportTemplateUrl(): SafeUrl {
583 return this.fileExport.safeUrl;
587 const entry: ComboboxEntry = this.copyTemplateCbox.selected;
588 if (!entry) { return; }
589 delete this.volcopy.templates[entry.id];
590 this.volcopy.saveTemplates();
591 this.copyTemplateCbox.selected = null;
594 displayAttr(field: string): boolean {
595 return this.volcopy.defaults.hidden[field] !== true;
598 copyFieldLabel(field: string): string {
599 const def = this.idl.classes.acp.field_map[field];
600 return def ? def.label : '';
603 // Returns false if any items are in magic statuses
604 statusEditable(): boolean {
605 const copies = this.context.copyList();
606 for (let idx = 0; idx < copies.length; idx++) {
607 if (this.volcopy.copyStatIsMagic(copies[idx].status())) {
614 // Called any time a change occurs that could affect the
615 // save-ability of the form.
618 const canSave = this.batchAttrs.filter(
619 attr => attr.warnOnRequired()).length === 0;
621 this.canSaveChange.emit(canSave);