1 import {Component, Input, OnInit, AfterViewInit, ViewChild,
2 EventEmitter, Output, QueryList, ViewChildren} from '@angular/core';
3 import {Router, ActivatedRoute} from '@angular/router';
4 import {SafeUrl} from '@angular/platform-browser';
5 import {IdlObject, IdlService} from '@eg/core/idl.service';
6 import {EventService} from '@eg/core/event.service';
7 import {OrgService} from '@eg/core/org.service';
8 import {StoreService} from '@eg/core/store.service';
9 import {NetService} from '@eg/core/net.service';
10 import {AuthService} from '@eg/core/auth.service';
11 import {PcrudService} from '@eg/core/pcrud.service';
12 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
13 import {VolCopyContext} from './volcopy';
14 import {VolCopyService} from './volcopy.service';
15 import {FormatService} from '@eg/core/format.service';
16 import {StringComponent} from '@eg/share/string/string.component';
17 import {CopyAlertsDialogComponent
18 } from '@eg/staff/share/holdings/copy-alerts-dialog.component';
19 import {CopyTagsDialogComponent
20 } from '@eg/staff/share/holdings/copy-tags-dialog.component';
21 import {CopyNotesDialogComponent
22 } from '@eg/staff/share/holdings/copy-notes-dialog.component';
23 import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
24 import {BatchItemAttrComponent, BatchChangeSelection
25 } from '@eg/staff/share/holdings/batch-item-attr.component';
26 import {FileExportService} from '@eg/share/util/file-export.service';
27 import {ToastService} from '@eg/share/toast/toast.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('savedHoldingsTemplates', {static: false})
75 savedHoldingsTemplates: StringComponent;
76 @ViewChild('deletedHoldingsTemplate', {static: false})
77 deletedHoldingsTemplate: StringComponent;
79 @ViewChild('copyAlertsDialog', {static: false})
80 private copyAlertsDialog: CopyAlertsDialogComponent;
82 @ViewChild('copyTagsDialog', {static: false})
83 private copyTagsDialog: CopyTagsDialogComponent;
85 @ViewChild('copyNotesDialog', {static: false})
86 private copyNotesDialog: CopyNotesDialogComponent;
88 @ViewChild('copyTemplateCbox', {static: false})
89 copyTemplateCbox: ComboboxComponent;
91 @ViewChildren(BatchItemAttrComponent)
92 batchAttrs: QueryList<BatchItemAttrComponent>;
94 // Emitted when the save-ability of this form changes.
95 @Output() canSaveChange: EventEmitter<boolean> = new EventEmitter<boolean>();
98 private router: Router,
99 private route: ActivatedRoute,
100 private evt: EventService,
101 private idl: IdlService,
102 private org: OrgService,
103 private net: NetService,
104 private auth: AuthService,
105 private pcrud: PcrudService,
106 private holdings: HoldingsService,
107 private format: FormatService,
108 private store: StoreService,
109 private fileExport: FileExportService,
110 private toast: ToastService,
111 public volcopy: VolCopyService
115 this.statCatFilter = this.volcopy.defaults.values.statcat_filter;
120 const tmpl = this.store.getLocalItem('cat.copy.last_template');
122 // avoid Express Changed warning w/ timeout
123 setTimeout(() => this.copyTemplateCbox.selectedId = tmpl);
126 this.loanDurationLabelMap[1] = this.loanDurationShort.text;
127 this.loanDurationLabelMap[2] = this.loanDurationNormal.text;
128 this.loanDurationLabelMap[3] = this.loanDurationLong.text;
130 this.fineLevelLabelMap[1] = this.fineLevelLow.text;
131 this.fineLevelLabelMap[2] = this.fineLevelNormal.text;
132 this.fineLevelLabelMap[3] = this.fineLevelHigh.text;
136 statCats(): IdlObject[] {
137 if (this.statCatFilter) {
138 const orgs = this.org.descendants(this.statCatFilter, true);
140 return this.volcopy.commonData.acp_stat_cat.filter(
141 sc => orgs.includes(sc.owner()));
145 return this.volcopy.commonData.acp_stat_cat;
150 orgSn(orgId: number): string {
151 return orgId ? this.org.get(orgId).shortname() : '';
154 statCatCounts(catId: number): {[value: string]: number} {
155 catId = Number(catId);
158 this.context.copyList().forEach(copy => {
159 const entry = copy.stat_cat_entries()
160 .filter(e => e.stat_cat() === catId)[0];
164 if (this.volcopy.statCatEntryMap[entry.id()]) {
165 value = this.volcopy.statCatEntryMap[entry.id()].value();
167 // Map to a remote stat cat. Ignore.
172 if (counts[value] === undefined) {
181 itemAttrCounts(field: string): {[value: string]: number} {
184 this.context.copyList().forEach(copy => {
185 const value = this.getFieldDisplayValue(field, copy);
187 if (counts[value] === undefined) {
196 getFieldDisplayValue(field: string, copy: IdlObject): string {
198 // Some fields don't live directly on the copy.
199 if (field === 'owning_lib') {
201 copy.call_number().owning_lib()).shortname() +
202 ' : ' + copy.call_number().label();
205 const value = copy[field]();
207 if (!value && value !== 0) { return ''; }
212 return this.volcopy.copyStatuses[value].name();
215 return value.name() +
216 ' (' + this.org.get(value.owning_lib()).shortname() + ')';
221 return this.format.transform(
222 {datatype: 'timestamp', value: value});
226 // VIEW_USER permission may be too narrow. If so,
227 // just display the user ID instead of the username.
228 return typeof value === 'object' ? value.usrname() : value;
231 return this.org.get(value).shortname();
234 const rule = this.volcopy.commonData.acp_age_protect.filter(
235 r => r.id() === Number(value))[0];
236 return rule ? rule.name() : '';
239 const grp = this.volcopy.commonData.acp_floating_group.filter(
240 g => g.id() === Number(value))[0];
241 return grp ? grp.name() : '';
243 case 'loan_duration':
244 return this.loanDurationLabelMap[value];
247 return this.fineLevelLabelMap[value];
250 const map = this.volcopy.commonData.acp_item_type_map.filter(
251 m => m.code() === value)[0];
252 return map ? map.value() : '';
254 case 'circ_modifier':
255 const mod = this.volcopy.commonData.acp_circ_modifier.filter(
256 m => m.code() === value)[0];
257 return mod ? mod.name() : '';
259 case 'mint_condition':
260 if (!this.mintConditionYes) { return ''; }
261 return value === 't' ?
262 this.mintConditionYes.text : this.mintConditionNo.text;
268 copyWantsChange(copy: IdlObject, field: string,
269 changeSelection: BatchChangeSelection): boolean {
270 const disValue = this.getFieldDisplayValue(field, copy);
271 return changeSelection[disValue] === true;
274 applyCopyValue(field: string, value?: any, changeSelection?: BatchChangeSelection) {
275 if (value === undefined) {
276 value = this.values[field];
278 this.values[field] = value;
281 if (field === 'owning_lib') {
282 this.owningLibChanged(value, changeSelection);
286 this.context.copyList().forEach(copy => {
287 if (!copy[field] || copy[field]() === value) { return; }
289 // Change selection indicates which items should be modified
290 // based on the display value for the selected field at
292 if (changeSelection &&
293 !this.copyWantsChange(copy, field, changeSelection)) {
298 copy.ischanged(true);
302 this.emitSaveChange();
305 owningLibChanged(orgId: number, changeSelection?: BatchChangeSelection) {
306 if (!orgId) { return; }
308 // Map existing vol IDs to their replacments.
309 const newVols: any = {};
311 this.context.copyList().forEach(copy => {
313 if (changeSelection &&
314 !this.copyWantsChange(copy, 'owning_lib', changeSelection)) {
318 // Change the copy circ lib to match the new owning lib
319 // if configured to do so.
320 if (this.volcopy.defaults.values.circ_lib_mod_with_owning_lib) {
321 if (copy.circ_lib() !== orgId) {
322 copy.circ_lib(orgId);
323 copy.ischanged(true);
326 .filter(ba => ba.name === 'circ_lib')
327 .forEach(attr => attr.hasChanged = true);
331 const vol = copy.call_number();
333 if (vol.owning_lib() === orgId) { return; } // No change needed
336 if (newVols[vol.id()]) {
337 newVol = newVols[vol.id()];
341 // The open-ils.cat.asset.volume.fleshed.batch.update API
342 // will use the existing volume when trying to create a
343 // new volume with the same parameters as an existing volume.
344 newVol = this.idl.clone(vol);
345 newVol.owning_lib(orgId);
346 newVol.id(this.volcopy.autoId--);
348 newVols[vol.id()] = newVol;
351 copy.call_number(newVol);
352 copy.ischanged(true);
354 this.context.removeCopyNode(copy.id());
355 this.context.findOrCreateCopyNode(copy);
358 // If any of the above actions results in an empty volume
359 // remove it from the tree. Note this does not delete the
360 // volume at the server, since other items could be attached
361 // of which this instance of the editor is not aware.
362 Object.keys(newVols).forEach(volId => {
364 const volNode = this.context.volNodes().filter(
365 node => node.target.id() === +volId)[0];
367 if (volNode && volNode.children.length === 0) {
368 this.context.removeVolNode(+volId);
373 // Create or modify a stat cat entry for each copy that does not
374 // already match the new value.
375 statCatChanged(catId: number, clear?: boolean) {
376 catId = Number(catId);
378 const entryId = this.statCatValues[catId];
380 if (!clear && (!entryId || !this.volcopy.statCatEntryMap[entryId])) {
382 `Attempt to apply stat cat value which does not exist.
383 This is likely the result of a stale copy template.
384 stat_cat=${catId} entry=${entryId}`);
389 this.context.copyList().forEach(copy => {
391 let entry = copy.stat_cat_entries()
392 .filter(e => e.stat_cat() === catId)[0];
397 // Removing the entry map (and setting copy.ishanged) is
398 // enough to tell the API to delete it.
400 copy.stat_cat_entries(copy.stat_cat_entries()
401 .filter(e => e.stat_cat() !== catId));
407 if (entry.id() === entryId) {
408 // Requested mapping already exists.
413 // Copy has no entry for this stat cat yet.
414 entry = this.idl.create('asce');
415 entry.stat_cat(catId);
416 copy.stat_cat_entries().push(entry);
420 entry.value(this.volcopy.statCatEntryMap[entryId].value());
423 copy.ischanged(true);
426 this.emitSaveChange();
430 this.copyAlertsDialog.inPlaceCreateMode = true;
431 this.copyAlertsDialog.copyIds = this.context.copyList().map(c => c.id());
433 this.copyAlertsDialog.open({size: 'lg'}).subscribe(changes => {
434 if (!changes) { return; }
436 if ((!changes.newAlerts || changes.newAlerts.length === 0) &&
437 (!changes.changedAlerts || changes.changedAlerts.length === 0)
442 if (changes.newAlerts) {
443 this.context.copyList().forEach(copy => {
444 changes.newAlerts.forEach(newAlert => {
445 const a = this.idl.clone(newAlert);
448 if (!copy.copy_alerts()) { copy.copy_alerts([]); }
449 copy.copy_alerts().push(a);
450 copy.ischanged(true);
454 if (changes.changedAlerts && this.context.copyList().length === 1) {
455 const copy = this.context.copyList()[0];
456 changes.changedAlerts.forEach(alert => {
457 const existing = copy.copy_alerts().filter(a => a.id() === alert.id())[0];
459 existing.ischanged(true);
460 existing.alert_type(alert.alert_type());
461 existing.temp(alert.temp());
462 existing.ack_time(alert.ack_time());
463 existing.ack_staff(alert.ack_staff());
464 copy.ischanged(true);
472 this.copyTagsDialog.inPlaceCreateMode = true;
473 this.copyTagsDialog.copyIds = this.context.copyList().map(c => c.id());
475 this.copyTagsDialog.open({size: 'lg'}).subscribe(changes => {
476 if ((!changes.newTags || changes.newTags.length === 0) &&
477 (!changes.deletedMaps || changes.deletedMaps.length === 0)) {
481 changes.newTags.forEach(tag => {
482 this.context.copyList().forEach(copy => {
484 if (copy.tags().filter(
485 m => m.tag() === tag.id()).length > 0) {
486 return; // map already exists
489 const map = this.idl.create('acptcm');
494 copy.tags().push(map);
495 copy.ischanged(true);
499 if (this.context.copyList().length === 1) {
500 const copy = this.context.copyList()[0];
501 changes.deletedMaps.forEach(tag => {
502 const existing = copy.tags().filter(t => t.id() === tag.id())[0];
504 existing.isdeleted(true);
505 copy.ischanged(true);
513 this.copyNotesDialog.inPlaceCreateMode = true;
514 this.copyNotesDialog.copyIds = this.context.copyList().map(c => c.id());
516 this.copyNotesDialog.open({size: 'lg'}).subscribe(changes => {
517 if (!changes) { return; }
519 if ((!changes.newNotes || changes.newNotes.length === 0) &&
520 (!changes.delNotes || changes.delNotes.length === 0)
525 changes.newNotes.forEach(note => {
526 this.context.copyList().forEach(copy => {
527 const n = this.idl.clone(note);
528 n.owning_copy(copy.id());
529 copy.notes().push(n);
530 copy.ischanged(true);
533 if (this.context.copyList().length === 1) {
534 const copy = this.context.copyList()[0];
535 changes.delNotes.forEach(note => {
536 const existing = copy.notes().filter(n => n.id() === note.id())[0];
538 existing.isdeleted(true);
539 copy.ischanged(true);
547 const entry = this.copyTemplateCbox.selected;
548 if (!entry) { return; }
550 this.store.setLocalItem('cat.copy.last_template', entry.id);
552 const template = this.volcopy.templates[entry.id];
554 Object.keys(template).forEach(field => {
555 const value = template[field];
557 if (value === null || value === undefined) { return; }
559 if (field === 'statcats') {
560 Object.keys(value).forEach(catId => {
561 if (value[+catId] !== null) {
562 this.statCatValues[+catId] = value[+catId];
563 this.statCatChanged(+catId);
569 // Copy alerts are stored as hashes of the bits we need.
570 // Templates can be used to create alerts, but not edit them.
571 if (field === 'copy_alerts' && Array.isArray(value)) {
573 this.context.copyList().forEach(copy => {
574 const newAlert = this.idl.create('aca');
575 newAlert.isnew(true);
576 newAlert.copy(copy.id());
577 newAlert.alert_type(a.alert_type);
578 newAlert.temp(a.temp);
579 newAlert.note(a.note);
580 newAlert.create_staff(this.auth.user().id());
581 newAlert.create_time('now');
583 if (Array.isArray(copy.copy_alerts())) {
584 copy.copy_alerts().push(newAlert);
586 copy.copy_alerts([newAlert]);
589 copy.ischanged(true);
596 // In some cases, we may have to fetch the data since
597 // the local code assumes copy field is fleshed.
598 let promise = Promise.resolve(value);
600 if (field === 'location') {
601 // May be a 'remote' location. Fetch as needed.
602 promise = this.volcopy.getLocation(value);
605 promise.then(val => {
606 this.applyCopyValue(field, val);
608 // Indicate in the form these values have changed
610 .filter(ba => ba.name === field)
611 .forEach(attr => attr.hasChanged = true);
617 const entry: ComboboxEntry = this.copyTemplateCbox.selected;
618 if (!entry) { return; }
623 if (entry.freetext) {
625 // freetext entries don't have an ID, but we may need one later.
626 entry.id = entry.label;
630 template = this.volcopy.templates[name];
633 this.batchAttrs.forEach(comp => {
634 if (!comp.hasChanged) { return; }
636 const field = comp.name;
637 const value = this.values[field];
639 if (value === null) {
640 delete template[field];
644 if (field.match(/stat_cat_/)) {
645 const statId = field.match(/stat_cat_(\d+)/)[1];
646 if (!template.statcats) { template.statcats = {}; }
648 template.statcats[statId] = value;
652 // Some values are fleshed. this assumes fleshed objects
653 // have an 'id' value, which is true so far.
655 typeof value === 'object' ? value.id() : value;
659 this.volcopy.templates[name] = template;
660 this.volcopy.saveTemplates().then(x => {
661 this.savedHoldingsTemplates.current().then(str => this.toast.success(str));
662 if (entry.freetext) {
663 // once a new template has been added, make it
664 // display like any other in the comobox
665 this.copyTemplateCbox.selected =
666 this.volcopy.templateNames.filter(_ => _.label === name)[0];
671 exportTemplate($event) {
672 if (this.fileExport.inProgress()) { return; }
674 this.fileExport.exportFile(
675 $event, JSON.stringify(this.volcopy.templates), 'text/json');
678 importTemplate($event) {
679 const file: File = $event.target.files[0];
680 if (!file) { return; }
682 const reader = new FileReader();
684 reader.addEventListener('load', () => {
687 const template = JSON.parse(reader.result as string);
688 var theKeys = Object.keys(template);
689 for(let i = 0; i < theKeys.length; i++){
690 var name = theKeys[i];
691 this.volcopy.templates[name]=template[name];
694 console.error('Invalid Item Attribute template', E);
698 this.volcopy.saveTemplates();
699 // Adds the new one to the list and re-sorts the labels.
700 this.volcopy.fetchTemplates();
703 reader.readAsText(file);
706 // Returns null when no export is in progress.
707 exportTemplateUrl(): SafeUrl {
708 return this.fileExport.safeUrl;
712 const entry: ComboboxEntry = this.copyTemplateCbox.selected;
713 if (!entry) { return; }
714 delete this.volcopy.templates[entry.id];
715 this.volcopy.saveTemplates().then(
716 x => this.deletedHoldingsTemplate.current().then(str => this.toast.success(str))
718 this.copyTemplateCbox.selected = null;
721 displayAttr(field: string): boolean {
722 return this.volcopy.defaults.hidden[field] !== true;
725 copyFieldLabel(field: string): string {
726 const def = this.idl.classes.acp.field_map[field];
727 return def ? def.label : '';
730 // Returns false if any items are in magic statuses
731 statusEditable(): boolean {
732 const copies = this.context.copyList();
733 for (let idx = 0; idx < copies.length; idx++) {
734 if (this.volcopy.copyStatIsMagic(copies[idx].status())) {
741 // Called any time a change occurs that could affect the
742 // save-ability of the form.
745 const canSave = this.batchAttrs.filter(
746 attr => attr.warnOnRequired()).length === 0;
748 this.canSaveChange.emit(canSave);
752 // True if one of our batch editors has been put into edit
753 // mode and left there without an Apply, Cancel, or Clear
754 hasActiveInput(): boolean {
755 return this.batchAttrs.filter(attr => attr.editing).length > 0;
758 applyPendingChanges() {
759 // If a user has left any changes in the 'editing' state, this
760 // will go through and apply the values so they do not have to
761 // click Apply for every one.
762 this.batchAttrs.filter(attr => attr.editing).forEach(attr => attr.save());
765 copyLocationOrgs(): number[] {
766 if (!this.context) { return []; }
768 // Make sure every org unit represented by the edit batch
770 const ids = this.context.orgNodes().map(n => n.target.id());
772 // Make sure all locations within the "full path" of our
773 // workstation org unit are included.
774 return ids.concat(this.org.fullPath(this.auth.user().ws_ou()));