LP#1775466 Angular(6) base application
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / share / admin-page / admin-page.component.ts
1 import {Component, Input, OnInit, TemplateRef, ViewChild} from '@angular/core';
2 import {IdlService, IdlObject} from '@eg/core/idl.service';
3 import {GridDataSource} from '@eg/share/grid/grid';
4 import {GridComponent} from '@eg/share/grid/grid.component';
5 import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
6 import {ToastService} from '@eg/share/toast/toast.service';
7 import {Pager} from '@eg/share/util/pager';
8 import {PcrudService} from '@eg/core/pcrud.service';
9 import {OrgService} from '@eg/core/org.service';
10 import {PermService} from '@eg/core/perm.service';
11 import {AuthService} from '@eg/core/auth.service';
12 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
13 import {StringComponent} from '@eg/share/string/string.component';
14
15 /**
16  * General purpose CRUD interface for IDL objects
17  *
18  * Object types using this component must be editable via PCRUD.
19  */
20
21 @Component({
22     selector: 'eg-admin-page',
23     templateUrl: './admin-page.component.html'
24 })
25
26 export class AdminPageComponent implements OnInit {
27
28     @Input() idlClass: string;
29
30     // Default sort field, used when no grid sorting is applied.
31     @Input() sortField: string;
32
33     // Data source may be provided by the caller.  This gives the caller
34     // complete control over the contents of the grid.  If no data source
35     // is provided, a generic one is create which is sufficient for data
36     // that requires no special handling, filtering, etc.
37     @Input() dataSource: GridDataSource;
38
39     // Size of create/edito dialog.  Uses large by default.
40     @Input() dialogSize: 'sm' | 'lg' = 'lg';
41
42     // If an org unit field is specified, an org unit filter
43     // is added to the top of the page.
44     @Input() orgField: string;
45
46     // Disable the auto-matic org unit field filter
47     @Input() disableOrgFilter: boolean;
48
49     // Include objects linking to org units which are ancestors
50     // of the selected org unit.
51     @Input() includeOrgAncestors: boolean;
52
53     // Ditto includeOrgAncestors, but descendants.
54     @Input() includeOrgDescendants: boolean;
55
56     // Optional grid persist key.  This is the part of the key
57     // following eg.grid.
58     @Input() persistKey: string;
59
60     // Optional path component to add to the generated grid persist key,
61     // formatted as (for example):
62     // 'eg.grid.admin.${persistKeyPfx}.config.billing_type'
63     @Input() persistKeyPfx: string;
64
65     @ViewChild('grid') grid: GridComponent;
66     @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
67     @ViewChild('successString') successString: StringComponent;
68     @ViewChild('createString') createString: StringComponent;
69     @ViewChild('translator') translator: TranslateComponent;
70
71     idlClassDef: any;
72     pkeyField: string;
73     createNew: () => void;
74     deleteSelected: (rows: IdlObject[]) => void;
75
76     // True if any columns on the object support translations
77     translateRowIdx: number;
78     translateFieldIdx: number;
79     translatableFields: string[];
80     translate: () => void;
81
82     contextOrg: IdlObject;
83     orgFieldLabel: string;
84     viewPerms: string;
85     canCreate: boolean;
86
87     constructor(
88         private idl: IdlService,
89         private org: OrgService,
90         private auth: AuthService,
91         private pcrud: PcrudService,
92         private perm: PermService,
93         private toast: ToastService
94     ) {
95         this.translatableFields = [];
96     }
97
98     applyOrgValues() {
99
100         if (this.disableOrgFilter) {
101             this.orgField = null;
102             return;
103         }
104
105         if (!this.orgField) {
106             // If no org unit field is specified, try to find one.
107             // If an object type has multiple org unit fields, the
108             // caller should specify one or disable org unit filter.
109             this.idlClassDef.fields.forEach(field => {
110                 if (field['class'] === 'aou') {
111                     this.orgField = field.name;
112                 }
113             });
114         }
115
116         if (this.orgField) {
117             this.orgFieldLabel = this.idlClassDef.field_map[this.orgField].label;
118             this.contextOrg = this.org.root();
119         }
120     }
121
122     ngOnInit() {
123         this.idlClassDef = this.idl.classes[this.idlClass];
124         this.pkeyField = this.idlClassDef.pkey || 'id';
125
126         this.translatableFields =
127             this.idlClassDef.fields.filter(f => f.i18n).map(f => f.name);
128
129         if (!this.persistKey) {
130             this.persistKey =
131                 'admin.' +
132                 (this.persistKeyPfx ? this.persistKeyPfx + '.' : '') +
133                 this.idlClassDef.table;
134         }
135
136         // Limit the view org selector to orgs where the user has
137         // permacrud-encoded view permissions.
138         const pc = this.idlClassDef.permacrud;
139         if (pc && pc.retrieve) {
140             this.viewPerms = pc.retrieve.perms;
141         }
142
143         this.checkCreatePerms();
144         this.applyOrgValues();
145
146         // If the caller provides not data source, create a generic one.
147         if (!this.dataSource) {
148             this.initDataSource();
149         }
150
151         // TODO: pass the row activate handler via the grid markup
152         this.grid.onRowActivate.subscribe(
153             (idlThing: IdlObject) => {
154                 this.editDialog.mode = 'update';
155                 this.editDialog.recId = idlThing[this.pkeyField]();
156                 this.editDialog.open({size: this.dialogSize}).then(
157                     ok => {
158                         this.successString.current()
159                             .then(str => this.toast.success(str));
160                         this.grid.reload();
161                     },
162                     err => {}
163                 );
164             }
165         );
166
167         this.createNew = () => {
168             this.editDialog.mode = 'create';
169             this.editDialog.open({size: this.dialogSize}).then(
170                 ok => {
171                     this.createString.current()
172                         .then(str => this.toast.success(str));
173                     this.grid.reload();
174                 },
175                 err => {}
176             );
177         };
178
179         this.deleteSelected = (idlThings: IdlObject[]) => {
180             idlThings.forEach(idlThing => idlThing.isdeleted(true));
181             this.pcrud.autoApply(idlThings).subscribe(
182                 val => console.debug('deleted: ' + val),
183                 err => {},
184                 ()  => this.grid.reload()
185             );
186         };
187
188         // Open the field translation dialog.
189         // Link the next/previous actions to cycle through each translatable
190         // field on each row.
191         this.translate = () => {
192             this.translateRowIdx = 0;
193             this.translateFieldIdx = 0;
194             this.translator.fieldName = this.translatableFields[this.translateFieldIdx];
195             this.translator.idlObject = this.dataSource.data[this.translateRowIdx];
196
197             this.translator.nextString = () => {
198
199                 if (this.translateFieldIdx < this.translatableFields.length - 1) {
200                     this.translateFieldIdx++;
201
202                 } else if (this.translateRowIdx < this.dataSource.data.length - 1) {
203                     this.translateRowIdx++;
204                     this.translateFieldIdx = 0;
205                 }
206
207                 this.translator.idlObject =
208                     this.dataSource.data[this.translateRowIdx];
209                 this.translator.fieldName =
210                     this.translatableFields[this.translateFieldIdx];
211             };
212
213             this.translator.prevString = () => {
214
215                 if (this.translateFieldIdx > 0) {
216                     this.translateFieldIdx--;
217
218                 } else if (this.translateRowIdx > 0) {
219                     this.translateRowIdx--;
220                     this.translateFieldIdx = 0;
221                 }
222
223                 this.translator.idlObject =
224                     this.dataSource.data[this.translateRowIdx];
225                 this.translator.fieldName =
226                     this.translatableFields[this.translateFieldIdx];
227             };
228
229             this.translator.open({size: 'lg'});
230         };
231     }
232
233     checkCreatePerms() {
234         this.canCreate = false;
235         const pc = this.idlClassDef.permacrud || {};
236         const perms = pc.create ? pc.create.perms : [];
237         if (perms.length === 0) { return; }
238
239         this.perm.hasWorkPermAt(perms, true).then(permMap => {
240             Object.keys(permMap).forEach(key => {
241                 if (permMap[key].length > 0) {
242                     this.canCreate = true;
243                 }
244             });
245         });
246     }
247
248     orgOnChange(org: IdlObject) {
249         this.contextOrg = org;
250         this.grid.reload();
251     }
252
253     initDataSource() {
254         this.dataSource = new GridDataSource();
255
256         this.dataSource.getRows = (pager: Pager, sort: any[]) => {
257             const orderBy: any = {};
258
259             if (sort.length) {
260                 // Sort specified from grid
261                 orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;
262
263             } else if (this.sortField) {
264                 // Default sort field
265                 orderBy[this.idlClass] = this.sortField;
266             }
267
268             const searchOps = {
269                 offset: pager.offset,
270                 limit: pager.limit,
271                 order_by: orderBy
272             };
273
274             if (this.contextOrg) {
275                 // Filter rows by those linking to the context org and
276                 // optionally ancestor and descendant org units.
277
278                 let orgs = [this.contextOrg.id()];
279
280                 if (this.includeOrgAncestors) {
281                     orgs = this.org.ancestors(this.contextOrg, true);
282                 }
283
284                 if (this.includeOrgDescendants) {
285                     // can result in duplicate workstation org IDs... meh
286                     orgs = orgs.concat(
287                         this.org.descendants(this.contextOrg, true));
288                 }
289
290                 const search = {};
291                 search[this.orgField] = orgs;
292                 return this.pcrud.search(this.idlClass, search, searchOps);
293             }
294
295             // No org filter -- fetch all rows
296             return this.pcrud.retrieveAll(this.idlClass, searchOps);
297         };
298     }
299
300     disableAncestorSelector(): boolean {
301         return this.contextOrg &&
302             this.contextOrg.id() === this.org.root().id();
303     }
304
305     disableDescendantSelector(): boolean {
306         return this.contextOrg && this.contextOrg.children().length === 0;
307     }
308
309 }
310
311