]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts
LP1888723 Item attributes editor can now clear stat cats
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / cat / volcopy / copy-attrs.component.ts
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';
26
27 @Component({
28   selector: 'eg-copy-attrs',
29   templateUrl: 'copy-attrs.component.html',
30
31   // Match the header of the batch attrs component
32   styles: [
33     `.batch-header {background-color: #EBF4FA;}`,
34     `.template-row {background-color: #EBF4FA;}`
35   ]
36 })
37 export class CopyAttrsComponent implements OnInit, AfterViewInit {
38
39     @Input() context: VolCopyContext;
40
41     // Batch values applied from the form.
42     // Some values are scalar, some IdlObjects depending on copy fleshyness.
43     values: {[field: string]: any} = {};
44
45     // Map of stat ID to entry ID.
46     statCatValues: {[statId: number]: number} = {};
47
48     loanDurationLabelMap: {[level: number]: string} = {};
49     fineLevelLabelMap: {[level: number]: string} = {};
50
51     statCatFilter: number;
52
53     @ViewChild('loanDurationShort', {static: false})
54         loanDurationShort: StringComponent;
55     @ViewChild('loanDurationNormal', {static: false})
56         loanDurationNormal: StringComponent;
57     @ViewChild('loanDurationLong', {static: false})
58         loanDurationLong: StringComponent;
59
60     @ViewChild('fineLevelLow', {static: false})
61         fineLevelLow: StringComponent;
62     @ViewChild('fineLevelNormal', {static: false})
63         fineLevelNormal: StringComponent;
64     @ViewChild('fineLevelHigh', {static: false})
65         fineLevelHigh: StringComponent;
66
67     @ViewChild('mintConditionYes', {static: false})
68         mintConditionYes: StringComponent;
69     @ViewChild('mintConditionNo', {static: false})
70         mintConditionNo: StringComponent;
71
72     @ViewChild('copyAlertsDialog', {static: false})
73         private copyAlertsDialog: CopyAlertsDialogComponent;
74
75     @ViewChild('copyTagsDialog', {static: false})
76         private copyTagsDialog: CopyTagsDialogComponent;
77
78     @ViewChild('copyTemplateCbox', {static: false})
79         copyTemplateCbox: ComboboxComponent;
80
81     @ViewChildren(BatchItemAttrComponent)
82         batchAttrs: QueryList<BatchItemAttrComponent>;
83
84     // Emitted when the save-ability of this form changes.
85     @Output() canSaveChange: EventEmitter<boolean> = new EventEmitter<boolean>();
86
87     constructor(
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
101     ) { }
102
103     ngOnInit() {
104         this.statCatFilter = this.volcopy.defaults.values.statcat_filter;
105     }
106
107     ngAfterViewInit() {
108
109         const tmpl = this.store.getLocalItem('cat.copy.last_template');
110         if (tmpl) {
111             // avoid Express Changed warning w/ timeout
112             setTimeout(() => this.copyTemplateCbox.selectedId = tmpl);
113         }
114
115         this.loanDurationLabelMap[1] = this.loanDurationShort.text;
116         this.loanDurationLabelMap[2] = this.loanDurationNormal.text;
117         this.loanDurationLabelMap[3] = this.loanDurationLong.text;
118
119         this.fineLevelLabelMap[1] = this.fineLevelLow.text;
120         this.fineLevelLabelMap[2] = this.fineLevelNormal.text;
121         this.fineLevelLabelMap[3] = this.fineLevelHigh.text;
122
123     }
124
125     statCats(): IdlObject[] {
126         if (this.statCatFilter) {
127             const orgs = this.org.descendants(this.statCatFilter, true);
128
129             return this.volcopy.commonData.acp_stat_cat.filter(
130                 sc => orgs.includes(sc.owner()));
131
132         } else {
133
134             return this.volcopy.commonData.acp_stat_cat;
135         }
136     }
137
138
139     orgSn(orgId: number): string {
140         return orgId ? this.org.get(orgId).shortname() : '';
141     }
142
143     statCatCounts(catId: number): {[value: string]: number} {
144         catId = Number(catId);
145         const counts = {};
146
147         this.context.copyList().forEach(copy => {
148             const entry = copy.stat_cat_entries()
149                 .filter(e => e.stat_cat() === catId)[0];
150
151             let value = '';
152             if (entry) {
153                 if (this.volcopy.statCatEntryMap[entry.id()]) {
154                     value = this.volcopy.statCatEntryMap[entry.id()].value();
155                 } else {
156                     // Map to a remote stat cat.  Ignore.
157                     return;
158                 }
159             }
160
161             if (counts[value] === undefined) {
162                 counts[value] = 0;
163             }
164             counts[value]++;
165         });
166
167         return counts;
168     }
169
170     itemAttrCounts(field: string): {[value: string]: number} {
171
172         const counts = {};
173         this.context.copyList().forEach(copy => {
174             const value = this.getFieldDisplayValue(field, copy);
175
176             if (counts[value] === undefined) {
177                 counts[value] = 0;
178             }
179             counts[value]++;
180         });
181
182         return counts;
183     }
184
185     getFieldDisplayValue(field: string, copy: IdlObject): string {
186
187         // Some fields don't live directly on the copy.
188         if (field === 'owning_lib') {
189             return this.org.get(
190                 copy.call_number().owning_lib()).shortname() +
191                 ' : ' + copy.call_number().label();
192         }
193
194         const value = copy[field]();
195
196         if (!value && value !== 0) { return ''; }
197
198         switch (field) {
199
200             case 'status':
201                 return this.volcopy.copyStatuses[value].name();
202
203             case 'location':
204                 return value.name() +
205                     ' (' + this.org.get(value.owning_lib()).shortname() + ')';
206
207             case 'edit_date':
208             case 'create_date':
209             case 'active_date':
210                 return this.format.transform(
211                     {datatype: 'timestamp', value: value});
212
213             case 'editor':
214             case 'creator':
215                 return value.usrname();
216
217             case 'circ_lib':
218                 return this.org.get(value).shortname();
219
220             case 'age_protect':
221                 const rule = this.volcopy.commonData.acp_age_protect.filter(
222                     r => r.id() === Number(value))[0];
223                 return rule ? rule.name() : '';
224
225             case 'floating':
226                 const grp = this.volcopy.commonData.acp_floating_group.filter(
227                     g => g.id() === Number(value))[0];
228                 return grp ? grp.name() : '';
229
230             case 'loan_duration':
231                 return this.loanDurationLabelMap[value];
232
233             case 'fine_level':
234                 return this.fineLevelLabelMap[value];
235
236             case 'circ_as_type':
237                 const map = this.volcopy.commonData.acp_item_type_map.filter(
238                     m => m.code() === value)[0];
239                 return map ? map.value() : '';
240
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() : '';
245
246             case 'mint_condition':
247                 if (!this.mintConditionYes) { return ''; }
248                 return value === 't' ?
249                     this.mintConditionYes.text : this.mintConditionNo.text;
250         }
251
252         return value;
253     }
254
255     copyWantsChange(copy: IdlObject, field: string,
256             changeSelection: BatchChangeSelection): boolean {
257         const disValue = this.getFieldDisplayValue(field, copy);
258         return changeSelection[disValue] === true;
259     }
260
261     applyCopyValue(field: string, value?: any, changeSelection?: BatchChangeSelection) {
262         if (value === undefined) {
263             value = this.values[field];
264         } else {
265             this.values[field] = value;
266         }
267
268         if (field === 'owning_lib') {
269             this.owningLibChanged(value, changeSelection);
270
271         } else {
272
273             this.context.copyList().forEach(copy => {
274                 if (!copy[field] || copy[field]() === value) { return; }
275
276                 // Change selection indicates which items should be modified
277                 // based on the display value for the selected field at
278                 // time of editing.
279                 if (changeSelection &&
280                     !this.copyWantsChange(copy, field, changeSelection)) {
281                     return;
282                 }
283
284                 copy[field](value);
285                 copy.ischanged(true);
286             });
287         }
288
289         this.emitSaveChange();
290     }
291
292     owningLibChanged(orgId: number, changeSelection?: BatchChangeSelection) {
293         if (!orgId) { return; }
294
295         // Map existing vol IDs to their replacments.
296         const newVols: any = {};
297
298         this.context.copyList().forEach(copy => {
299
300             if (changeSelection &&
301                 !this.copyWantsChange(copy, 'owning_lib', changeSelection)) {
302                 return;
303             }
304
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);
311
312                     this.batchAttrs
313                         .filter(ba => ba.name === 'circ_lib')
314                         .forEach(attr => attr.hasChanged = true);
315                 }
316             }
317
318             const vol = copy.call_number();
319
320             if (vol.owning_lib() === orgId) { return; } // No change needed
321
322             let newVol;
323             if (newVols[vol.id()]) {
324                 newVol = newVols[vol.id()];
325
326             } else {
327
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--);
334                 newVol.isnew(true);
335                 newVols[vol.id()] = newVol;
336             }
337
338             copy.call_number(newVol);
339             copy.ischanged();
340
341             this.context.removeCopyNode(copy.id());
342             this.context.findOrCreateCopyNode(copy);
343         });
344
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 => {
350
351             const volNode = this.context.volNodes().filter(
352                 node => node.target.id() === +volId)[0];
353
354             if (volNode && volNode.children.length === 0) {
355                 this.context.removeVolNode(+volId);
356             }
357         });
358     }
359
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);
364
365         const entryId = this.statCatValues[catId];
366
367         if (!clear && (!entryId || !this.volcopy.statCatEntryMap[entryId])) {
368             console.warn(
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}`);
372
373             return;
374         }
375
376         this.context.copyList().forEach(copy => {
377
378             let entry = copy.stat_cat_entries()
379                 .filter(e => e.stat_cat() === catId)[0];
380
381             if (clear) {
382
383                 if (entry) {
384                     // Removing the entry map (and setting copy.ishanged) is
385                     // enough to tell the API to delete it.
386
387                     copy.stat_cat_entries(copy.stat_cat_entries()
388                         .filter(e => e.stat_cat() !== catId));
389                 }
390
391             } else {
392
393                 if (entry) {
394                     if (entry.id() === entryId) {
395                         // Requested mapping already exists.
396                         return;
397                     }
398                 } else {
399
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);
404                 }
405
406                 entry.id(entryId);
407                 entry.value(this.volcopy.statCatEntryMap[entryId].value());
408             }
409
410             copy.ischanged(true);
411         });
412     }
413
414     openCopyAlerts() {
415         this.copyAlertsDialog.inPlaceMode = true;
416         this.copyAlertsDialog.copyIds = this.context.copyList().map(c => c.id());
417
418         this.copyAlertsDialog.open({size: 'lg'}).subscribe(
419             newAlert => {
420                 if (newAlert) {
421                     this.context.copyList().forEach(copy => {
422                         const a = this.idl.clone(newAlert);
423                         a.isnew(true);
424                         a.copy(copy.id());
425                         if (!copy.copy_alerts()) { copy.copy_alerts([]); }
426                         copy.copy_alerts().push(a);
427                         copy.ischanged(true);
428                     });
429                 }
430             }
431         );
432     }
433
434     openCopyTags() {
435         this.copyTagsDialog.inPlaceMode = true;
436         this.copyTagsDialog.copyIds = this.context.copyList().map(c => c.id());
437
438         this.copyTagsDialog.open({size: 'lg'}).subscribe(newTags => {
439             if (!newTags || newTags.length === 0) { return; }
440
441             newTags.forEach(tag => {
442                 this.context.copyList().forEach(copy => {
443
444                     if (copy.tags().filter(
445                         m => m.tag().id() === tag.id()).length > 0) {
446                         return; // map already exists
447                     }
448
449                     const map = this.idl.create('acptcm');
450                     map.isnew(true);
451                     map.copy(copy.id());
452                     map.tag(tag);
453
454                     copy.tags().push(map);
455                     copy.ischanged(true);
456                 });
457             });
458         });
459     }
460
461     applyTemplate() {
462         const entry = this.copyTemplateCbox.selected;
463         if (!entry) { return; }
464
465         this.store.setLocalItem('cat.copy.last_template', entry.id);
466
467         const template = this.volcopy.templates[entry.id];
468
469         Object.keys(template).forEach(field => {
470             const value = template[field];
471
472             if (value === null || value === undefined) { return; }
473
474             if (field === 'statcats') {
475                 Object.keys(value).forEach(catId => {
476                     this.statCatValues[+catId] = value[+catId];
477                     this.statCatChanged(+catId);
478                 });
479                 return;
480             }
481
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);
485
486             if (field === 'location') {
487                 // May be a 'remote' location.  Fetch as needed.
488                 promise = this.volcopy.getLocation(value);
489             }
490
491             promise.then(val => {
492                 this.applyCopyValue(field, val);
493
494                 // Indicate in the form these values have changed
495                 this.batchAttrs
496                     .filter(ba => ba.name === field)
497                     .forEach(attr => attr.hasChanged = true);
498             });
499         });
500     }
501
502     saveTemplate() {
503         const entry: ComboboxEntry = this.copyTemplateCbox.selected;
504         if (!entry) { return; }
505
506         let name;
507         let template;
508
509         if (entry.freetext) {
510             name = entry.label;
511             // freetext entries don't have an ID, but we may need one later.
512             entry.id = entry.label;
513             template = {};
514         } else {
515             name = entry.id;
516             template = this.volcopy.templates[name];
517         }
518
519         this.batchAttrs.forEach(comp => {
520             if (!comp.hasChanged) { return; }
521
522             const field = comp.name;
523             const value = this.values[field];
524
525             if (value === null) {
526                 delete template[field];
527                 return;
528             }
529
530             if (field.match(/stat_cat_/)) {
531                 const statId = field.match(/stat_cat_(\d+)/)[1];
532                 if (!template.statcats) { template.statcats = {}; }
533
534                 template.statcats[statId] = value;
535
536             } else {
537
538                 // Some values are fleshed. this assumes fleshed objects
539                 // have an 'id' value, which is true so far.
540                 template[field] =
541                     typeof value === 'object' ?  value.id() : value;
542             }
543         });
544
545         this.volcopy.templates[name] = template;
546         this.volcopy.saveTemplates();
547     }
548
549     exportTemplate($event) {
550         if (this.fileExport.inProgress()) { return; }
551
552         this.fileExport.exportFile(
553             $event, JSON.stringify(this.volcopy.templates), 'text/json');
554     }
555
556     importTemplate($event) {
557         const file: File = $event.target.files[0];
558         if (!file) { return; }
559
560         const reader = new FileReader();
561
562         reader.addEventListener('load', () => {
563
564             try {
565                 const template = JSON.parse(reader.result as string);
566                 const name = Object.keys(template)[0];
567                 this.volcopy.templates[name] = template[name];
568             } catch (E) {
569                 console.error('Invalid Item Attribute template', E);
570                 return;
571             }
572
573             this.volcopy.saveTemplates();
574             // Adds the new one to the list and re-sorts the labels.
575             this.volcopy.fetchTemplates();
576         });
577
578         reader.readAsText(file);
579     }
580
581     // Returns null when no export is in progress.
582     exportTemplateUrl(): SafeUrl {
583         return this.fileExport.safeUrl;
584     }
585
586     deleteTemplate() {
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;
592     }
593
594     displayAttr(field: string): boolean {
595         return this.volcopy.defaults.hidden[field] !== true;
596     }
597
598     copyFieldLabel(field: string): string {
599         const def = this.idl.classes.acp.field_map[field];
600         return def ? def.label : '';
601     }
602
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())) {
608                 return false;
609             }
610         }
611         return true;
612     }
613
614     // Called any time a change occurs that could affect the
615     // save-ability of the form.
616     emitSaveChange() {
617         setTimeout(() => {
618             const canSave = this.batchAttrs.filter(
619                 attr => attr.warnOnRequired()).length === 0;
620
621             this.canSaveChange.emit(canSave);
622         });
623     }
624 }
625
626
627