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