From: Bill Erickson Date: Mon, 12 Aug 2019 20:16:01 +0000 (-0400) Subject: LP1840050 org unit admin UI WIP X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=f3ff7e224981febe0dc91c1ecbab05eb47ba066e;p=working%2FEvergreen.git LP1840050 org unit admin UI WIP Angular port of Admin => Server Administration => Organizational Units. Initial UI component. Adds IDL labels and required attributes for Org Unit Addresses Org Select sanity checks on selected org being unset Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index d28d7084d7..cb2b8bf157 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -6205,17 +6205,17 @@ SELECT usr, - - - + + + - - + + - + - + @@ -6742,14 +6742,14 @@ SELECT usr, - - + + - + diff --git a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html index d49217c6fb..32bc94e2b9 100644 --- a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html +++ b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.html @@ -4,7 +4,7 @@ {{r.label}} - + {{selected.label}} diff --git a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts index dee3248939..dc14debcbf 100644 --- a/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts +++ b/Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts @@ -182,7 +182,7 @@ export class OrgSelectComponent implements OnInit { } // Remove the tree-padding spaces when matching. - formatter = (result: OrgDisplay) => result.label.trim(); + formatter = (result: OrgDisplay) => result ? result.label.trim() : ''; // reset the state of the component reset() { diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html index 99cb4781b7..7c62f20cfe 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html @@ -76,7 +76,7 @@ + routerLink="/staff/admin/server/actor/org_unit"> + + +Update Succeeded + + +Update Failed + + + + + + + {{org.name()}} ({{org.shortname()}}) + + + +
+
+

Org Units

+ +
+
+
+
+ {{currentOrg().name()}} ({{currentOrg().shortname()}}) +
+
+ + + +
+ + +
+
+
+ + +
+
+
Open Time
+
Close Time
+
+
+
+ Monday + Tuesday + Wednesday + Thursday + Friday + Saturday + Sunday +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit.component.ts new file mode 100644 index 0000000000..6afef79a8a --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit.component.ts @@ -0,0 +1,232 @@ +import {Component, Input, ViewChild, OnInit} from '@angular/core'; +import {Tree, TreeNode} from '@eg/share/tree/tree'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap'; +import {OrgService} from '@eg/core/org.service'; +import {AuthService} from '@eg/core/auth.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {StringService} from '@eg/share/string/string.service'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; + +@Component({ + templateUrl: './org-unit.component.html' +}) +export class OrgUnitComponent implements OnInit { + + tree: Tree; + selected: TreeNode; + @ViewChild('editString') editString: StringComponent; + @ViewChild('errorString') errorString: StringComponent; + @ViewChild('delConfirm') delConfirm: ConfirmDialogComponent; + + constructor( + private idl: IdlService, + private org: OrgService, + private auth: AuthService, + private pcrud: PcrudService, + private strings: StringService, + private toast: ToastService + ) {} + + + ngOnInit() { + this.loadAouTree(this.org.root().id()); + } + + tabChanged(evt: NgbTabChangeEvent) { + const tab = evt.nextId; + // stubbing out in case we need it. + } + + orgSaved(orgId: number) { + if (!orgId && this.currentOrg()) { + orgId = this.currentOrg().id(); + } + this.loadAouTree(orgId).then(_ => this.postUpdate(this.editString)); + } + + loadAouTree(selectNodeId?: number): Promise { + return this.pcrud.search('aou', {parent_ou : null}, + {flesh : -1, flesh_fields : {aou : [ + 'children', 'ou_type', 'hours_of_operation', 'ill_address', + 'holds_address', 'mailing_address', 'billing_address' + ]}}, {authoritative: true} + ).toPromise().then(tree => { + this.ingestAouTree(tree); + if (selectNodeId) { + const node = this.tree.findNode(selectNodeId); + this.selected = node; + this.tree.selectNode(node); + } + }); + } + + // Translate the org unt type tree into a structure EgTree can use. + ingestAouTree(aouTree) { + + const handleNode = (orgNode: IdlObject): TreeNode => { + if (!orgNode) { return; } + + if (!orgNode.hours_of_operation()) { + this.generateHours(orgNode); + } + + this.addAddresses(orgNode); + + const treeNode = new TreeNode({ + id: orgNode.id(), + label: orgNode.name(), + callerData: {orgUnit: orgNode} + }); + + // Apply the compiled label asynchronously + this.strings.interpolate( + 'admin.server.org_unit.treenode', {org: orgNode} + ).then(label => treeNode.label = label); + + orgNode.children().forEach(childNode => + treeNode.children.push(handleNode(childNode)) + ); + + return treeNode; + }; + + const rootNode = handleNode(aouTree); + this.tree = new Tree(rootNode); + } + + nodeClicked($event: any) { + this.selected = $event; + } + + // Fills the org unit in with 'new' addresses for any fields + // that are missing. + addAddresses(org: IdlObject) { + ['billing_address', 'mailing_address', 'holds_address', 'ill_address'] + .forEach(addrType => { + if (org[addrType]()) { return ; } + const addr = this.idl.create('aoa'); + addr.isnew(true); + addr.valid('t'); + addr.org_unit(org.id()); + org[addrType](addr); + }); + } + + generateHours(org: IdlObject) { + const hours = this.idl.create('aouhoo'); + hours.id(org.id()); + hours.isnew(true); + + [0, 1, 2, 3, 4, 5, 6].forEach(dow => { + this.hours(dow, 'open', '09:00:00', hours); + this.hours(dow, 'close', '17:00:00', hours); + }); + + org.hours_of_operation(hours); + } + + // if a 'value' is passed, it will be applied to the optional + // hours-of-operation object, otherwise the hours on the currently + // selected org unit. + hours(dow: number, which: 'open' | 'close', value?: string, hoo?: IdlObject): string { + if (!hoo && !this.selected) { return null; } + + const hours = hoo || this.selected.callerData.orgUnit.hours_of_operation(); + + if (value) { + hours[`dow_${dow}_${which}`](value); + hours.ischanged(true); + } + + return hours[`dow_${dow}_${which}`](); + } + + isClosed(dow: number): boolean { + return ( + this.hours(dow, 'open') === '00:00:00' && + this.hours(dow, 'close') === '00:00:00' + ); + } + + closedOn(dow: number) { + this.hours(dow, 'open', '00:00:00'); + this.hours(dow, 'close', '00:00:00'); + } + + saveHours() { + const org = this.currentOrg(); + const hours = org.hours_of_operation(); + this.pcrud.autoApply(hours).subscribe( + result => { + console.debug('Hours saved ', result); + this.editString.current() + .then(msg => this.toast.success(msg)); + }, + error => { + this.errorString.current() + .then(msg => this.toast.danger(msg)); + }, + () => this.loadAouTree(this.selected.id) + ); + } + + currentOrg(): IdlObject { + return this.selected ? this.selected.callerData.orgUnit : null; + } + + postUpdate(message: StringComponent) { + // Modifying org unit types means refetching the org unit + // data normally fetched on page load, since it includes + // org unit type data. + this.org.fetchOrgs().then(() => + message.current().then(str => this.toast.success(str))); + } + + remove() { + this.delConfirm.open().subscribe(confirmed => { + if (!confirmed) { return; } + + const org = this.selected.callerData.orgUnit; + + this.pcrud.remove(org).subscribe( + ok2 => {}, + err => { + this.errorString.current() + .then(str => this.toast.danger(str)); + }, + () => { + // Avoid updating until we know the entire + // pcrud action/transaction completed. + // After removal, select the parent org if available + // otherwise the root org. + const orgId = org.parent_ou() ? + org.parent_ou() : this.org.root().id(); + this.loadAouTree(orgId).then(_ => + this.postUpdate(this.editString)); + } + ); + }); + } + + addChild() { + const parentTreeNode = this.selected; + const parentOrg = parentTreeNode.callerData.orgUnit; + + const org = this.idl.create('aou'); + org.isnew(true); + org.parent(parentOrg.id()); + // TODO: limit org type selector to types at parent depth + 1 + + // Create a dummy, detached org node to keep the UI happy. + this.selected = new TreeNode({ + id: org.id(), + label: org.name(), + callerData: {orgUnit: org} + }); + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts index c971ed74a7..aa4ec3184b 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts @@ -3,6 +3,7 @@ import {RouterModule, Routes} from '@angular/router'; import {AdminServerSplashComponent} from './admin-server-splash.component'; import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component'; import {OrgUnitTypeComponent} from './org-unit-type.component'; +import {OrgUnitComponent} from './org-unit.component'; const routes: Routes = [{ path: 'splash', @@ -11,6 +12,9 @@ const routes: Routes = [{ path: 'actor/org_unit_type', component: OrgUnitTypeComponent }, { + path: 'actor/org_unit', + component: OrgUnitComponent +}, { path: ':schema/:table', component: BasicAdminPageComponent }];