]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts
LP19800544 Import all holdings templates
[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} 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';
28
29 @Component({
30   selector: 'eg-copy-attrs',
31   templateUrl: 'copy-attrs.component.html',
32
33   // Match the header of the batch attrs component
34   styles: [
35     `.batch-header {background-color: #EBF4FA;}`,
36     `.template-row {background-color: #EBF4FA;}`
37   ]
38 })
39 export class CopyAttrsComponent implements OnInit, AfterViewInit {
40
41     @Input() context: VolCopyContext;
42
43     // Batch values applied from the form.
44     // Some values are scalar, some IdlObjects depending on copy fleshyness.
45     values: {[field: string]: any} = {};
46
47     // Map of stat ID to entry ID.
48     statCatValues: {[statId: number]: number} = {};
49
50     loanDurationLabelMap: {[level: number]: string} = {};
51     fineLevelLabelMap: {[level: number]: string} = {};
52
53     statCatFilter: number;
54
55     @ViewChild('loanDurationShort', {static: false})
56         loanDurationShort: StringComponent;
57     @ViewChild('loanDurationNormal', {static: false})
58         loanDurationNormal: StringComponent;
59     @ViewChild('loanDurationLong', {static: false})
60         loanDurationLong: StringComponent;
61
62     @ViewChild('fineLevelLow', {static: false})
63         fineLevelLow: StringComponent;
64     @ViewChild('fineLevelNormal', {static: false})
65         fineLevelNormal: StringComponent;
66     @ViewChild('fineLevelHigh', {static: false})
67         fineLevelHigh: StringComponent;
68
69     @ViewChild('mintConditionYes', {static: false})
70         mintConditionYes: StringComponent;
71     @ViewChild('mintConditionNo', {static: false})
72         mintConditionNo: StringComponent;
73
74     @ViewChild('savedHoldingsTemplates', {static: false})
75         savedHoldingsTemplates: StringComponent;
76     @ViewChild('deletedHoldingsTemplate', {static: false})
77         deletedHoldingsTemplate: StringComponent;
78
79     @ViewChild('copyAlertsDialog', {static: false})
80         private copyAlertsDialog: CopyAlertsDialogComponent;
81
82     @ViewChild('copyTagsDialog', {static: false})
83         private copyTagsDialog: CopyTagsDialogComponent;
84
85     @ViewChild('copyNotesDialog', {static: false})
86         private copyNotesDialog: CopyNotesDialogComponent;
87
88     @ViewChild('copyTemplateCbox', {static: false})
89         copyTemplateCbox: ComboboxComponent;
90
91     @ViewChildren(BatchItemAttrComponent)
92         batchAttrs: QueryList<BatchItemAttrComponent>;
93
94     // Emitted when the save-ability of this form changes.
95     @Output() canSaveChange: EventEmitter<boolean> = new EventEmitter<boolean>();
96
97     constructor(
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
112     ) { }
113
114     ngOnInit() {
115         this.statCatFilter = this.volcopy.defaults.values.statcat_filter;
116     }
117
118     ngAfterViewInit() {
119
120         const tmpl = this.store.getLocalItem('cat.copy.last_template');
121         if (tmpl) {
122             // avoid Express Changed warning w/ timeout
123             setTimeout(() => this.copyTemplateCbox.selectedId = tmpl);
124         }
125
126         this.loanDurationLabelMap[1] = this.loanDurationShort.text;
127         this.loanDurationLabelMap[2] = this.loanDurationNormal.text;
128         this.loanDurationLabelMap[3] = this.loanDurationLong.text;
129
130         this.fineLevelLabelMap[1] = this.fineLevelLow.text;
131         this.fineLevelLabelMap[2] = this.fineLevelNormal.text;
132         this.fineLevelLabelMap[3] = this.fineLevelHigh.text;
133
134     }
135
136     statCats(): IdlObject[] {
137         if (this.statCatFilter) {
138             const orgs = this.org.descendants(this.statCatFilter, true);
139
140             return this.volcopy.commonData.acp_stat_cat.filter(
141                 sc => orgs.includes(sc.owner()));
142
143         } else {
144
145             return this.volcopy.commonData.acp_stat_cat;
146         }
147     }
148
149
150     orgSn(orgId: number): string {
151         return orgId ? this.org.get(orgId).shortname() : '';
152     }
153
154     statCatCounts(catId: number): {[value: string]: number} {
155         catId = Number(catId);
156         const counts = {};
157
158         this.context.copyList().forEach(copy => {
159             const entry = copy.stat_cat_entries()
160                 .filter(e => e.stat_cat() === catId)[0];
161
162             let value = '';
163             if (entry) {
164                 if (this.volcopy.statCatEntryMap[entry.id()]) {
165                     value = this.volcopy.statCatEntryMap[entry.id()].value();
166                 } else {
167                     // Map to a remote stat cat.  Ignore.
168                     return;
169                 }
170             }
171
172             if (counts[value] === undefined) {
173                 counts[value] = 0;
174             }
175             counts[value]++;
176         });
177
178         return counts;
179     }
180
181     itemAttrCounts(field: string): {[value: string]: number} {
182
183         const counts = {};
184         this.context.copyList().forEach(copy => {
185             const value = this.getFieldDisplayValue(field, copy);
186
187             if (counts[value] === undefined) {
188                 counts[value] = 0;
189             }
190             counts[value]++;
191         });
192
193         return counts;
194     }
195
196     getFieldDisplayValue(field: string, copy: IdlObject): string {
197
198         // Some fields don't live directly on the copy.
199         if (field === 'owning_lib') {
200             return this.org.get(
201                 copy.call_number().owning_lib()).shortname() +
202                 ' : ' + copy.call_number().label();
203         }
204
205         const value = copy[field]();
206
207         if (!value && value !== 0) { return ''; }
208
209         switch (field) {
210
211             case 'status':
212                 return this.volcopy.copyStatuses[value].name();
213
214             case 'location':
215                 return value.name() +
216                     ' (' + this.org.get(value.owning_lib()).shortname() + ')';
217
218             case 'edit_date':
219             case 'create_date':
220             case 'active_date':
221                 return this.format.transform(
222                     {datatype: 'timestamp', value: value});
223
224             case 'editor':
225             case 'creator':
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;
229
230             case 'circ_lib':
231                 return this.org.get(value).shortname();
232
233             case 'age_protect':
234                 const rule = this.volcopy.commonData.acp_age_protect.filter(
235                     r => r.id() === Number(value))[0];
236                 return rule ? rule.name() : '';
237
238             case 'floating':
239                 const grp = this.volcopy.commonData.acp_floating_group.filter(
240                     g => g.id() === Number(value))[0];
241                 return grp ? grp.name() : '';
242
243             case 'loan_duration':
244                 return this.loanDurationLabelMap[value];
245
246             case 'fine_level':
247                 return this.fineLevelLabelMap[value];
248
249             case 'circ_as_type':
250                 const map = this.volcopy.commonData.acp_item_type_map.filter(
251                     m => m.code() === value)[0];
252                 return map ? map.value() : '';
253
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() : '';
258
259             case 'mint_condition':
260                 if (!this.mintConditionYes) { return ''; }
261                 return value === 't' ?
262                     this.mintConditionYes.text : this.mintConditionNo.text;
263         }
264
265         return value;
266     }
267
268     copyWantsChange(copy: IdlObject, field: string,
269             changeSelection: BatchChangeSelection): boolean {
270         const disValue = this.getFieldDisplayValue(field, copy);
271         return changeSelection[disValue] === true;
272     }
273
274     applyCopyValue(field: string, value?: any, changeSelection?: BatchChangeSelection) {
275         if (value === undefined) {
276             value = this.values[field];
277         } else {
278             this.values[field] = value;
279         }
280
281         if (field === 'owning_lib') {
282             this.owningLibChanged(value, changeSelection);
283
284         } else {
285
286             this.context.copyList().forEach(copy => {
287                 if (!copy[field] || copy[field]() === value) { return; }
288
289                 // Change selection indicates which items should be modified
290                 // based on the display value for the selected field at
291                 // time of editing.
292                 if (changeSelection &&
293                     !this.copyWantsChange(copy, field, changeSelection)) {
294                     return;
295                 }
296
297                 copy[field](value);
298                 copy.ischanged(true);
299             });
300         }
301
302         this.emitSaveChange();
303     }
304
305     owningLibChanged(orgId: number, changeSelection?: BatchChangeSelection) {
306         if (!orgId) { return; }
307
308         // Map existing vol IDs to their replacments.
309         const newVols: any = {};
310
311         this.context.copyList().forEach(copy => {
312
313             if (changeSelection &&
314                 !this.copyWantsChange(copy, 'owning_lib', changeSelection)) {
315                 return;
316             }
317
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);
324
325                     this.batchAttrs
326                         .filter(ba => ba.name === 'circ_lib')
327                         .forEach(attr => attr.hasChanged = true);
328                 }
329             }
330
331             const vol = copy.call_number();
332
333             if (vol.owning_lib() === orgId) { return; } // No change needed
334
335             let newVol;
336             if (newVols[vol.id()]) {
337                 newVol = newVols[vol.id()];
338
339             } else {
340
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--);
347                 newVol.isnew(true);
348                 newVols[vol.id()] = newVol;
349             }
350
351             copy.call_number(newVol);
352             copy.ischanged(true);
353
354             this.context.removeCopyNode(copy.id());
355             this.context.findOrCreateCopyNode(copy);
356         });
357
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 => {
363
364             const volNode = this.context.volNodes().filter(
365                 node => node.target.id() === +volId)[0];
366
367             if (volNode && volNode.children.length === 0) {
368                 this.context.removeVolNode(+volId);
369             }
370         });
371     }
372
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);
377
378         const entryId = this.statCatValues[catId];
379
380         if (!clear && (!entryId || !this.volcopy.statCatEntryMap[entryId])) {
381             console.warn(
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}`);
385
386             return;
387         }
388
389         this.context.copyList().forEach(copy => {
390
391             let entry = copy.stat_cat_entries()
392                 .filter(e => e.stat_cat() === catId)[0];
393
394             if (clear) {
395
396                 if (entry) {
397                     // Removing the entry map (and setting copy.ishanged) is
398                     // enough to tell the API to delete it.
399
400                     copy.stat_cat_entries(copy.stat_cat_entries()
401                         .filter(e => e.stat_cat() !== catId));
402                 }
403
404             } else {
405
406                 if (entry) {
407                     if (entry.id() === entryId) {
408                         // Requested mapping already exists.
409                         return;
410                     }
411                 } else {
412
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);
417                 }
418
419                 entry.id(entryId);
420                 entry.value(this.volcopy.statCatEntryMap[entryId].value());
421             }
422
423             copy.ischanged(true);
424         });
425
426         this.emitSaveChange();
427     }
428
429     openCopyAlerts() {
430         this.copyAlertsDialog.inPlaceCreateMode = true;
431         this.copyAlertsDialog.copyIds = this.context.copyList().map(c => c.id());
432
433         this.copyAlertsDialog.open({size: 'lg'}).subscribe(changes => {
434             if (!changes) { return; }
435
436             if ((!changes.newAlerts || changes.newAlerts.length === 0) &&
437                 (!changes.changedAlerts || changes.changedAlerts.length === 0)
438                ) {
439                 return;
440             }
441
442             if (changes.newAlerts) {
443                 this.context.copyList().forEach(copy => {
444                     changes.newAlerts.forEach(newAlert => {
445                         const a = this.idl.clone(newAlert);
446                         a.isnew(true);
447                         a.copy(copy.id());
448                         if (!copy.copy_alerts()) { copy.copy_alerts([]); }
449                         copy.copy_alerts().push(a);
450                         copy.ischanged(true);
451                     });
452                 });
453             }
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];
458                     if (existing) {
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);
465                     }
466                 });
467             }
468         });
469     }
470
471     openCopyTags() {
472         this.copyTagsDialog.inPlaceCreateMode = true;
473         this.copyTagsDialog.copyIds = this.context.copyList().map(c => c.id());
474
475         this.copyTagsDialog.open({size: 'lg'}).subscribe(changes => {
476             if ((!changes.newTags || changes.newTags.length === 0) &&
477                 (!changes.deletedMaps || changes.deletedMaps.length === 0)) {
478                 return;
479             }
480
481             changes.newTags.forEach(tag => {
482                 this.context.copyList().forEach(copy => {
483
484                     if (copy.tags().filter(
485                         m => m.tag() === tag.id()).length > 0) {
486                         return; // map already exists
487                     }
488
489                     const map = this.idl.create('acptcm');
490                     map.isnew(true);
491                     map.copy(copy.id());
492                     map.tag(tag);
493
494                     copy.tags().push(map);
495                     copy.ischanged(true);
496                 });
497             });
498
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];
503                     if (existing) {
504                         existing.isdeleted(true);
505                         copy.ischanged(true);
506                     }
507                 });
508             }
509         });
510     }
511
512     openCopyNotes() {
513         this.copyNotesDialog.inPlaceCreateMode = true;
514         this.copyNotesDialog.copyIds = this.context.copyList().map(c => c.id());
515
516         this.copyNotesDialog.open({size: 'lg'}).subscribe(changes => {
517             if (!changes) { return; }
518
519             if ((!changes.newNotes || changes.newNotes.length === 0) &&
520                 (!changes.delNotes || changes.delNotes.length === 0)
521                ) {
522                 return;
523             }
524
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);
531                 });
532             });
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];
537                     if (existing) {
538                         existing.isdeleted(true);
539                         copy.ischanged(true);
540                     }
541                 });
542             }
543         });
544     }
545
546     applyTemplate() {
547         const entry = this.copyTemplateCbox.selected;
548         if (!entry) { return; }
549
550         this.store.setLocalItem('cat.copy.last_template', entry.id);
551
552         const template = this.volcopy.templates[entry.id];
553
554         Object.keys(template).forEach(field => {
555             const value = template[field];
556
557             if (value === null || value === undefined) { return; }
558
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);
564                     }
565                 });
566                 return;
567             }
568
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)) {
572                 value.forEach(a => {
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');
582
583                         if (Array.isArray(copy.copy_alerts())) {
584                             copy.copy_alerts().push(newAlert);
585                         } else {
586                             copy.copy_alerts([newAlert]);
587                         }
588
589                         copy.ischanged(true);
590                     });
591                 });
592
593                 return;
594             }
595
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);
599
600             if (field === 'location') {
601                 // May be a 'remote' location.  Fetch as needed.
602                 promise = this.volcopy.getLocation(value);
603             }
604
605             promise.then(val => {
606                 this.applyCopyValue(field, val);
607
608                 // Indicate in the form these values have changed
609                 this.batchAttrs
610                     .filter(ba => ba.name === field)
611                     .forEach(attr => attr.hasChanged = true);
612             });
613         });
614     }
615
616     saveTemplate() {
617         const entry: ComboboxEntry = this.copyTemplateCbox.selected;
618         if (!entry) { return; }
619
620         let name;
621         let template;
622
623         if (entry.freetext) {
624             name = entry.label;
625             // freetext entries don't have an ID, but we may need one later.
626             entry.id = entry.label;
627             template = {};
628         } else {
629             name = entry.id;
630             template = this.volcopy.templates[name];
631         }
632
633         this.batchAttrs.forEach(comp => {
634             if (!comp.hasChanged) { return; }
635
636             const field = comp.name;
637             const value = this.values[field];
638
639             if (value === null) {
640                 delete template[field];
641                 return;
642             }
643
644             if (field.match(/stat_cat_/)) {
645                 const statId = field.match(/stat_cat_(\d+)/)[1];
646                 if (!template.statcats) { template.statcats = {}; }
647
648                 template.statcats[statId] = value;
649
650             } else {
651
652                 // Some values are fleshed. this assumes fleshed objects
653                 // have an 'id' value, which is true so far.
654                 template[field] =
655                     typeof value === 'object' ?  value.id() : value;
656             }
657         });
658
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];
667             }
668         });
669     }
670
671     exportTemplate($event) {
672         if (this.fileExport.inProgress()) { return; }
673
674         this.fileExport.exportFile(
675             $event, JSON.stringify(this.volcopy.templates), 'text/json');
676     }
677
678     importTemplate($event) {
679         const file: File = $event.target.files[0];
680         if (!file) { return; }
681
682         const reader = new FileReader();
683
684         reader.addEventListener('load', () => {
685
686             try {
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];
692                 };
693                         } catch (E) {
694                 console.error('Invalid Item Attribute template', E);
695                 return;
696             }
697
698             this.volcopy.saveTemplates();
699             // Adds the new one to the list and re-sorts the labels.
700             this.volcopy.fetchTemplates();
701         });
702
703         reader.readAsText(file);
704     }
705
706     // Returns null when no export is in progress.
707     exportTemplateUrl(): SafeUrl {
708         return this.fileExport.safeUrl;
709     }
710
711     deleteTemplate() {
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))
717         );
718         this.copyTemplateCbox.selected = null;
719     }
720
721     displayAttr(field: string): boolean {
722         return this.volcopy.defaults.hidden[field] !== true;
723     }
724
725     copyFieldLabel(field: string): string {
726         const def = this.idl.classes.acp.field_map[field];
727         return def ? def.label : '';
728     }
729
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())) {
735                 return false;
736             }
737         }
738         return true;
739     }
740
741     // Called any time a change occurs that could affect the
742     // save-ability of the form.
743     emitSaveChange() {
744         setTimeout(() => {
745             const canSave = this.batchAttrs.filter(
746                 attr => attr.warnOnRequired()).length === 0;
747
748             this.canSaveChange.emit(canSave);
749         });
750     }
751
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;
756     }
757
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());
763     }
764
765     copyLocationOrgs(): number[] {
766         if (!this.context) { return []; }
767
768         // Make sure every org unit represented by the edit batch
769         // is represented.
770         const ids = this.context.orgNodes().map(n => n.target.id());
771
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()));
775     }
776 }
777
778
779