1 /* eslint-disable eqeqeq, max-len, no-magic-numbers */
2 import {Component, ViewChild, OnInit} from '@angular/core';
3 import {Tree, TreeNode} from '@eg/share/tree/tree';
4 import {IdlService, IdlObject} from '@eg/core/idl.service';
5 import {NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
6 import {OrgService} from '@eg/core/org.service';
7 import {AuthService} from '@eg/core/auth.service';
8 import {PcrudService} from '@eg/core/pcrud.service';
9 import {ToastService} from '@eg/share/toast/toast.service';
10 import {StringComponent} from '@eg/share/string/string.component';
11 import {StringService} from '@eg/share/string/string.service';
12 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
13 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
14 import {PermService} from '@eg/core/perm.service';
17 templateUrl: './org-unit.component.html',
18 styleUrls: [ './org-unit.component.css' ],
20 export class OrgUnitComponent implements OnInit {
26 hasClosedDatePerms: boolean;
28 @ViewChild('editString', { static: true }) editString: StringComponent;
29 @ViewChild('errorString', { static: true }) errorString: StringComponent;
30 @ViewChild('delConfirm', { static: true }) delConfirm: ConfirmDialogComponent;
33 private idl: IdlService,
34 private org: OrgService,
35 private auth: AuthService,
36 private pcrud: PcrudService,
37 private strings: StringService,
38 private toast: ToastService,
39 private perm: PermService,
44 this.loadAouTree(this.org.root().id());
46 // Check once on init if user could be linked to closed date editor (don't want them to land on a page that does nothing and think it's broken)
47 const neededClosedDatesPerms = ['actor.org_unit.closed_date.create',
48 'actor.org_unit.closed_date.update',
49 'actor.org_unit.closed_date.delete'];
51 this.perm.hasWorkPermAt(neededClosedDatesPerms, true).then((perm) => {
52 // Set true once if they have every permission they need to change closed dates
53 this.hasClosedDatePerms = neededClosedDatesPerms.every(element => {
54 return perm[element].length > 0;
60 navChanged(evt: NgbNavChangeEvent) {
61 const tab = evt.nextId;
62 // stubbing out in case we need it.
65 orgSaved(orgId: number | IdlObject) {
68 if (orgId) { // new org created, focus it.
69 id = typeof orgId === 'object' ? orgId.id() : orgId;
70 } else if (this.currentOrg()) {
71 id = this.currentOrg().id();
74 this.loadAouTree(id).then(_ => this.postUpdate(this.editString));
81 loadAouTree(selectNodeId?: number): Promise<any> {
83 const flesh = ['children', 'ou_type', 'hours_of_operation'];
85 return this.pcrud.search('aou', {parent_ou : null},
86 {flesh : -1, flesh_fields : {aou : flesh}}, {authoritative: true}
88 ).toPromise().then(tree => {
89 this.ingestAouTree(tree);
90 if (!selectNodeId) { selectNodeId = this.org.root().id(); }
92 const node = this.tree.findNode(selectNodeId);
94 this.tree.selectNode(node);
98 // Translate the org unt type tree into a structure EgTree can use.
99 ingestAouTree(aouTree) {
101 const handleNode = (orgNode: IdlObject, expand?: boolean): TreeNode => {
102 if (!orgNode) { return; }
104 if (!orgNode.hours_of_operation()) {
105 this.generateHours(orgNode);
108 const treeNode = new TreeNode({
110 label: orgNode.name(),
111 callerData: {orgUnit: orgNode},
115 // Apply the compiled label asynchronously
116 this.strings.interpolate(
117 'admin.server.org_unit.treenode', {org: orgNode}
118 ).then(label => treeNode.label = label);
120 // Tree node labels are "name -- shortname". Sorting
121 // by name suffices and bypasses the need the wait
122 // for all of the labels to interpolate.
124 .sort((a, b) => a.name() < b.name() ? -1 : 1)
125 .forEach(childNode =>
126 treeNode.children.push(handleNode(childNode))
132 const rootNode = handleNode(aouTree, true);
133 this.tree = new Tree(rootNode);
136 nodeClicked($event: any) {
137 this.selected = $event;
140 generateHours(org: IdlObject) {
141 const hours = this.idl.create('aouhoo');
145 [0, 1, 2, 3, 4, 5, 6].forEach(dow => {
146 this.hours(dow, 'open', '09:00:00', hours);
147 this.hours(dow, 'close', '17:00:00', hours);
150 org.hours_of_operation(hours);
153 // if a 'value' is passed, it will be applied to the optional
154 // hours-of-operation object, otherwise the hours on the currently
155 // selected org unit.
156 hours(dow: number, which: 'open' | 'close' | 'note', value?: string, hoo?: IdlObject): string {
157 if (!hoo && !this.selected) { return null; }
159 const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
162 hours[`dow_${dow}_${which}`](value);
163 hours.ischanged(true);
166 return hours[`dow_${dow}_${which}`]();
169 isClosed(dow: number): boolean {
171 this.hours(dow, 'open') === '00:00:00' &&
172 this.hours(dow, 'close') === '00:00:00'
176 // Is the org closed every day of the week?
177 allClosed(): boolean{
178 return [0, 1, 2, 3, 4, 5, 6].every(dow => this.isClosed(dow));
181 getNote(dow: number, hoo?: IdlObject) {
182 if (!hoo && !this.selected) { return null; }
184 const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
186 return hours['dow_' + dow + '_note']();
189 setNote(dow: number, value?: string, hoo?: IdlObject) {
191 if (!hoo && !this.selected) { return null; }
193 const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
195 hours['dow_' + dow + '_note'](value);
196 hours.ischanged(true);
198 return hours['dow_' + dow + '_note']();
201 note(dow: number, which: 'note', value?: string, hoo?: IdlObject) {
202 if (!hoo && !this.selected) { return null; }
204 const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation();
206 hours[`dow_${dow}_${which}`]('');
207 hours.ischanged(true);
208 } else if (value != hours[`dow_${dow}_${which}`]()) {
209 hours[`dow_${dow}_${which}`](value);
210 hours.ischanged(true);
212 return hours[`dow_${dow}_${which}`]();
215 closedOn(dow: number) {
216 this.hours(dow, 'open', '00:00:00');
217 this.hours(dow, 'close', '00:00:00');
221 const org = this.currentOrg();
222 const hours = org.hours_of_operation();
223 this.pcrud.autoApply(hours).subscribe(
225 console.debug('Hours saved ', result);
226 this.editString.current()
227 .then(msg => this.toast.success(msg));
229 (error: unknown) => {
230 this.errorString.current()
231 .then(msg => this.toast.danger(msg));
233 () => this.loadAouTree(this.selected.id)
238 const hours = this.currentOrg().hours_of_operation();
239 const promise = hours.isnew() ? Promise.resolve() :
240 this.pcrud.remove(hours).toPromise();
242 promise.then(_ => this.generateHours(this.currentOrg()));
245 currentOrg(): IdlObject {
246 return this.selected ? this.selected.callerData.orgUnit : null;
249 orgHasChildren(): boolean {
250 const org = this.currentOrg();
251 return (org && org.children().length > 0);
254 postUpdate(message: StringComponent) {
255 // Modifying org unit types means refetching the org unit
256 // data normally fetched on page load, since it includes
257 // org unit type data.
258 this.org.fetchOrgs().then(() =>
259 message.current().then(str => this.toast.success(str)));
263 this.delConfirm.open().subscribe(confirmed => {
264 if (!confirmed) { return; }
266 const org = this.selected.callerData.orgUnit;
268 // eslint-disable-next-line rxjs/no-nested-subscribe
269 this.pcrud.remove(org).subscribe(
272 this.errorString.current()
273 .then(str => this.toast.danger(str));
276 // Avoid updating until we know the entire
277 // pcrud action/transaction completed.
278 // After removal, select the parent org if available
279 // otherwise the root org.
280 const orgId = org.parent_ou() ?
281 org.parent_ou() : this.org.root().id();
282 this.loadAouTree(orgId).then(_ =>
283 this.postUpdate(this.editString));
289 orgTypeOptions(): ComboboxEntry[] {
290 let ouType = this.currentOrg().ou_type();
292 if (typeof ouType === 'number') {
293 // May not be fleshed for new org units
294 ouType = this.org.typeMap()[ouType];
296 const curDepth = ouType.depth();
298 return this.org.typeList()
299 .filter(type_ => type_.depth() === curDepth)
300 .map(type_ => ({id: type_.id(), label: type_.name()}));
303 orgChildTypes(): IdlObject[] {
304 let ouType = this.currentOrg().ou_type();
306 if (typeof ouType === 'number') {
307 // May not be fleshed for new org units
308 ouType = this.org.typeMap()[ouType];
311 const depth = ouType.depth();
312 return this.org.typeList()
313 .filter(type_ => type_.depth() === depth + 1);
317 const parentTreeNode = this.selected;
318 const parentOrg = this.currentOrg();
319 const newType = this.orgChildTypes()[0];
321 const org = this.idl.create('aou');
323 org.parent_ou(parentOrg.id());
324 org.ou_type(newType.id());
327 // Create a dummy, detached org node to keep the UI happy.
328 this.selected = new TreeNode({
331 callerData: {orgUnit: org}
335 addressChanged(thing: any) {
336 // Reload to pick up org unit address changes.
337 this.orgSaved(this.currentOrg().id());