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 return value.usrname();
229 return this.org.get(value).shortname();
232 const rule = this.volcopy.commonData.acp_age_protect.filter(
233 r => r.id() === Number(value))[0];
234 return rule ? rule.name() : '';
237 const grp = this.volcopy.commonData.acp_floating_group.filter(
238 g => g.id() === Number(value))[0];
239 return grp ? grp.name() : '';
241 case 'loan_duration':
242 return this.loanDurationLabelMap[value];
245 return this.fineLevelLabelMap[value];
248 const map = this.volcopy.commonData.acp_item_type_map.filter(
249 m => m.code() === value)[0];
250 return map ? map.value() : '';
252 case 'circ_modifier':
253 const mod = this.volcopy.commonData.acp_circ_modifier.filter(
254 m => m.code() === value)[0];
255 return mod ? mod.name() : '';
257 case 'mint_condition':
258 if (!this.mintConditionYes) { return ''; }
259 return value === 't' ?
260 this.mintConditionYes.text : this.mintConditionNo.text;
266 copyWantsChange(copy: IdlObject, field: string,
267 changeSelection: BatchChangeSelection): boolean {
268 const disValue = this.getFieldDisplayValue(field, copy);
269 return changeSelection[disValue] === true;
272 applyCopyValue(field: string, value?: any, changeSelection?: BatchChangeSelection) {
273 if (value === undefined) {
274 value = this.values[field];
276 this.values[field] = value;
279 if (field === 'owning_lib') {
280 this.owningLibChanged(value, changeSelection);
284 this.context.copyList().forEach(copy => {
285 if (!copy[field] || copy[field]() === value) { return; }
287 // Change selection indicates which items should be modified
288 // based on the display value for the selected field at
290 if (changeSelection &&
291 !this.copyWantsChange(copy, field, changeSelection)) {
296 copy.ischanged(true);
300 this.emitSaveChange();
303 owningLibChanged(orgId: number, changeSelection?: BatchChangeSelection) {
304 if (!orgId) { return; }
306 // Map existing vol IDs to their replacments.
307 const newVols: any = {};
309 this.context.copyList().forEach(copy => {
311 if (changeSelection &&
312 !this.copyWantsChange(copy, 'owning_lib', changeSelection)) {
316 // Change the copy circ lib to match the new owning lib
317 // if configured to do so.
318 if (this.volcopy.defaults.values.circ_lib_mod_with_owning_lib) {
319 if (copy.circ_lib() !== orgId) {
320 copy.circ_lib(orgId);
321 copy.ischanged(true);
324 .filter(ba => ba.name === 'circ_lib')
325 .forEach(attr => attr.hasChanged = true);
329 const vol = copy.call_number();
331 if (vol.owning_lib() === orgId) { return; } // No change needed
334 if (newVols[vol.id()]) {
335 newVol = newVols[vol.id()];
339 // The open-ils.cat.asset.volume.fleshed.batch.update API
340 // will use the existing volume when trying to create a
341 // new volume with the same parameters as an existing volume.
342 newVol = this.idl.clone(vol);
343 newVol.owning_lib(orgId);
344 newVol.id(this.volcopy.autoId--);
346 newVols[vol.id()] = newVol;
349 copy.call_number(newVol);
350 copy.ischanged(true);
352 this.context.removeCopyNode(copy.id());
353 this.context.findOrCreateCopyNode(copy);
356 // If any of the above actions results in an empty volume
357 // remove it from the tree. Note this does not delete the
358 // volume at the server, since other items could be attached
359 // of which this instance of the editor is not aware.
360 Object.keys(newVols).forEach(volId => {
362 const volNode = this.context.volNodes().filter(
363 node => node.target.id() === +volId)[0];
365 if (volNode && volNode.children.length === 0) {
366 this.context.removeVolNode(+volId);
371 // Create or modify a stat cat entry for each copy that does not
372 // already match the new value.
373 statCatChanged(catId: number, clear?: boolean) {
374 catId = Number(catId);
376 const entryId = this.statCatValues[catId];
378 if (!clear && (!entryId || !this.volcopy.statCatEntryMap[entryId])) {
380 `Attempt to apply stat cat value which does not exist.
381 This is likely the result of a stale copy template.
382 stat_cat=${catId} entry=${entryId}`);
387 this.context.copyList().forEach(copy => {
389 let entry = copy.stat_cat_entries()
390 .filter(e => e.stat_cat() === catId)[0];
395 // Removing the entry map (and setting copy.ishanged) is
396 // enough to tell the API to delete it.
398 copy.stat_cat_entries(copy.stat_cat_entries()
399 .filter(e => e.stat_cat() !== catId));
405 if (entry.id() === entryId) {
406 // Requested mapping already exists.
411 // Copy has no entry for this stat cat yet.
412 entry = this.idl.create('asce');
413 entry.stat_cat(catId);
414 copy.stat_cat_entries().push(entry);
418 entry.value(this.volcopy.statCatEntryMap[entryId].value());
421 copy.ischanged(true);
424 this.emitSaveChange();
428 this.copyAlertsDialog.inPlaceCreateMode = true;
429 this.copyAlertsDialog.copyIds = this.context.copyList().map(c => c.id());
431 this.copyAlertsDialog.open({size: 'lg'}).subscribe(changes => {
432 if (!changes) { return; }
434 if ((!changes.newAlerts || changes.newAlerts.length === 0) &&
435 (!changes.changedAlerts || changes.changedAlerts.length === 0)
440 if (changes.newAlerts) {
441 this.context.copyList().forEach(copy => {
442 changes.newAlerts.forEach(newAlert => {
443 const a = this.idl.clone(newAlert);
446 if (!copy.copy_alerts()) { copy.copy_alerts([]); }
447 copy.copy_alerts().push(a);
448 copy.ischanged(true);
452 if (changes.changedAlerts && this.context.copyList().length === 1) {
453 const copy = this.context.copyList()[0];
454 changes.changedAlerts.forEach(alert => {
455 const existing = copy.copy_alerts().filter(a => a.id() === alert.id())[0];
457 existing.ischanged(true);
458 existing.alert_type(alert.alert_type());
459 existing.temp(alert.temp());
460 existing.ack_time(alert.ack_time());
461 existing.ack_staff(alert.ack_staff());
462 copy.ischanged(true);
470 this.copyTagsDialog.inPlaceCreateMode = true;
471 this.copyTagsDialog.copyIds = this.context.copyList().map(c => c.id());
473 this.copyTagsDialog.open({size: 'lg'}).subscribe(changes => {
474 if ((!changes.newTags || changes.newTags.length === 0) &&
475 (!changes.deletedMaps || changes.deletedMaps.length === 0)) {
479 changes.newTags.forEach(tag => {
480 this.context.copyList().forEach(copy => {
482 if (copy.tags().filter(
483 m => m.tag() === tag.id()).length > 0) {
484 return; // map already exists
487 const map = this.idl.create('acptcm');
492 copy.tags().push(map);
493 copy.ischanged(true);
497 if (this.context.copyList().length === 1) {
498 const copy = this.context.copyList()[0];
499 changes.deletedMaps.forEach(tag => {
500 const existing = copy.tags().filter(t => t.id() === tag.id())[0];
502 existing.isdeleted(true);
503 copy.ischanged(true);
511 this.copyNotesDialog.inPlaceCreateMode = true;
512 this.copyNotesDialog.copyIds = this.context.copyList().map(c => c.id());
514 this.copyNotesDialog.open({size: 'lg'}).subscribe(changes => {
515 if (!changes) { return; }
517 if ((!changes.newNotes || changes.newNotes.length === 0) &&
518 (!changes.delNotes || changes.delNotes.length === 0)
523 changes.newNotes.forEach(note => {
524 this.context.copyList().forEach(copy => {
525 const n = this.idl.clone(note);
526 n.owning_copy(copy.id());
527 copy.notes().push(n);
528 copy.ischanged(true);
531 if (this.context.copyList().length === 1) {
532 const copy = this.context.copyList()[0];
533 changes.delNotes.forEach(note => {
534 const existing = copy.notes().filter(n => n.id() === note.id())[0];
536 existing.isdeleted(true);
537 copy.ischanged(true);
545 const entry = this.copyTemplateCbox.selected;
546 if (!entry) { return; }
548 this.store.setLocalItem('cat.copy.last_template', entry.id);
550 const template = this.volcopy.templates[entry.id];
552 Object.keys(template).forEach(field => {
553 const value = template[field];
555 if (value === null || value === undefined) { return; }
557 if (field === 'statcats') {
558 Object.keys(value).forEach(catId => {
559 if (value[+catId] !== null) {
560 this.statCatValues[+catId] = value[+catId];
561 this.statCatChanged(+catId);
567 // Copy alerts are stored as hashes of the bits we need.
568 // Templates can be used to create alerts, but not edit them.
569 if (field === 'copy_alerts' && Array.isArray(value)) {
571 this.context.copyList().forEach(copy => {
572 const newAlert = this.idl.create('aca');
573 newAlert.isnew(true);
574 newAlert.copy(copy.id());
575 newAlert.alert_type(a.alert_type);
576 newAlert.temp(a.temp);
577 newAlert.note(a.note);
578 newAlert.create_staff(this.auth.user().id());
579 newAlert.create_time('now');
581 if (Array.isArray(copy.copy_alerts())) {
582 copy.copy_alerts().push(newAlert);
584 copy.copy_alerts([newAlert]);
587 copy.ischanged(true);
594 // In some cases, we may have to fetch the data since
595 // the local code assumes copy field is fleshed.
596 let promise = Promise.resolve(value);
598 if (field === 'location') {
599 // May be a 'remote' location. Fetch as needed.
600 promise = this.volcopy.getLocation(value);
603 promise.then(val => {
604 this.applyCopyValue(field, val);
606 // Indicate in the form these values have changed
608 .filter(ba => ba.name === field)
609 .forEach(attr => attr.hasChanged = true);
615 const entry: ComboboxEntry = this.copyTemplateCbox.selected;
616 if (!entry) { return; }
621 if (entry.freetext) {
623 // freetext entries don't have an ID, but we may need one later.
624 entry.id = entry.label;
628 template = this.volcopy.templates[name];
631 this.batchAttrs.forEach(comp => {
632 if (!comp.hasChanged) { return; }
634 const field = comp.name;
635 const value = this.values[field];
637 if (value === null) {
638 delete template[field];
642 if (field.match(/stat_cat_/)) {
643 const statId = field.match(/stat_cat_(\d+)/)[1];
644 if (!template.statcats) { template.statcats = {}; }
646 template.statcats[statId] = value;
650 // Some values are fleshed. this assumes fleshed objects
651 // have an 'id' value, which is true so far.
653 typeof value === 'object' ? value.id() : value;
657 this.volcopy.templates[name] = template;
658 this.volcopy.saveTemplates().then(x => {
659 this.savedHoldingsTemplates.current().then(str => this.toast.success(str));
660 if (entry.freetext) {
661 // once a new template has been added, make it
662 // display like any other in the comobox
663 this.copyTemplateCbox.selected =
664 this.volcopy.templateNames.filter(_ => _.label === name)[0];
669 exportTemplate($event) {
670 if (this.fileExport.inProgress()) { return; }
672 this.fileExport.exportFile(
673 $event, JSON.stringify(this.volcopy.templates), 'text/json');
676 importTemplate($event) {
677 const file: File = $event.target.files[0];
678 if (!file) { return; }
680 const reader = new FileReader();
682 reader.addEventListener('load', () => {
685 const template = JSON.parse(reader.result as string);
686 const name = Object.keys(template)[0];
687 this.volcopy.templates[name] = template[name];
689 console.error('Invalid Item Attribute template', E);
693 this.volcopy.saveTemplates();
694 // Adds the new one to the list and re-sorts the labels.
695 this.volcopy.fetchTemplates();
698 reader.readAsText(file);
701 // Returns null when no export is in progress.
702 exportTemplateUrl(): SafeUrl {
703 return this.fileExport.safeUrl;
707 const entry: ComboboxEntry = this.copyTemplateCbox.selected;
708 if (!entry) { return; }
709 delete this.volcopy.templates[entry.id];
710 this.volcopy.saveTemplates().then(
711 x => this.deletedHoldingsTemplate.current().then(str => this.toast.success(str))
713 this.copyTemplateCbox.selected = null;
716 displayAttr(field: string): boolean {
717 return this.volcopy.defaults.hidden[field] !== true;
720 copyFieldLabel(field: string): string {
721 const def = this.idl.classes.acp.field_map[field];
722 return def ? def.label : '';
725 // Returns false if any items are in magic statuses
726 statusEditable(): boolean {
727 const copies = this.context.copyList();
728 for (let idx = 0; idx < copies.length; idx++) {
729 if (this.volcopy.copyStatIsMagic(copies[idx].status())) {
736 // Called any time a change occurs that could affect the
737 // save-ability of the form.
740 const canSave = this.batchAttrs.filter(
741 attr => attr.warnOnRequired()).length === 0;
743 this.canSaveChange.emit(canSave);
747 // True if one of our batch editors has been put into edit
748 // mode and left there without an Apply, Cancel, or Clear
749 hasActiveInput(): boolean {
750 return this.batchAttrs.filter(attr => attr.editing).length > 0;
753 applyPendingChanges() {
754 // If a user has left any changes in the 'editing' state, this
755 // will go through and apply the values so they do not have to
756 // click Apply for every one.
757 this.batchAttrs.filter(attr => attr.editing).forEach(attr => attr.save());
760 copyLocationOrgs(): number[] {
761 if (!this.context) { return []; }
763 // Make sure every org unit represented by the edit batch
765 const ids = this.context.orgNodes().map(n => n.target.id());
767 // Make sure all locations within the "full path" of our
768 // workstation org unit are included.
769 return ids.concat(this.org.fullPath(this.auth.user().ws_ou()));