LP1840050 org unit admin UI WIP user/berick/lp1840050-org-unit-admin-wip-2
authorBill Erickson <berickxx@gmail.com>
Thu, 15 Aug 2019 15:40:23 +0000 (11:40 -0400)
committerBill Erickson <berickxx@gmail.com>
Fri, 16 Aug 2019 20:40:32 +0000 (16:40 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts
Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts
Open-ILS/src/eg2/src/app/staff/admin/server/org-addr.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/org-addr.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/org-unit-routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/org-unit.component.html
Open-ILS/src/eg2/src/app/staff/admin/server/org-unit.component.ts
Open-ILS/src/eg2/src/app/staff/admin/server/org-unit.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts

index 9e7dc25..7edfdf4 100644 (file)
@@ -72,7 +72,7 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit {
     // Allow the selected entry ID to be passed via the template
     // This does NOT not emit onChange events.
     @Input() set selectedId(id: any) {
-        if (id) { 
+        if (id) {
             if (this.entrylist.length) {
                 this.selected = this.entrylist.filter(e => e.id === id)[0];
             }
index a7fe825..27a2c99 100644 (file)
@@ -1,17 +1,14 @@
 import {NgModule} from '@angular/core';
 import {TreeModule} from '@eg/share/tree/tree.module';
-import {StaffCommonModule} from '@eg/staff/common.module';
-import {AdminServerRoutingModule} from './routing.module';
 import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {AdminServerRoutingModule} from './routing.module';
 import {AdminServerSplashComponent} from './admin-server-splash.component';
 import {OrgUnitTypeComponent} from './org-unit-type.component';
-import {OrgUnitComponent} from './org-unit.component';
 
 @NgModule({
   declarations: [
       AdminServerSplashComponent,
-      OrgUnitTypeComponent,
-      OrgUnitComponent
+      OrgUnitTypeComponent
   ],
   imports: [
     AdminCommonModule,
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/org-addr.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/org-addr.component.html
new file mode 100644 (file)
index 0000000..b3a787b
--- /dev/null
@@ -0,0 +1,36 @@
+
+<ngb-tabset #addressTabs *ngIf="orgUnit" (tabChange)="tabChanged($event)">
+  <ng-container *ngFor="let type of addrTypes()">
+    <b>type = {{type}}</b>
+
+    <ngb-tab *ngIf="addr(type)"
+      i18n-title id="{{type}}"
+      title="{{type === 'billing_address' ? 'Physical Address' : 
+        (type === 'holds_address' ? 'Holds Address' : 
+        (type === 'mailing_address' ? 'Mailing Address' : 'ILL Address'))}}">
+
+      <ng-template ngbTabContent>
+        <eg-fm-record-editor idlClass="aoa" readonlyFields="org_unit" 
+          [mode]="addr(type).isnew() ? 'create': 'update'" 
+          [hideBanner]="true" displayMode="inline" hiddenFields="id"
+          (recordSaved)="addrSaved($event)" 
+          [record]="addr(type)"
+          fieldOrder="address_type,street1,street2,city,county,state,country,post_code,san,valid"
+          >
+          <eg-fm-record-editor-action i18n-label label="Delete" *ngIf="!addr(type).isnew()"
+            (actionClick)="deleteAddress($event)" buttonCss="btn-warning">
+          </eg-fm-record-editor-action>
+        </eg-fm-record-editor>
+
+        <ng-container *ngIf="sharedAddress(addr(type).id())">
+          <div class="alert alert-info">
+            <span i18n>This address is used for multiple address types.</span>
+            <button (click)="cloneAddress(type)" 
+              class="btn btn-light ml-3" i18n>Clone As New Address</button>
+          </div>
+        </ng-container>
+      </ng-template>
+    </ngb-tab>
+  </ng-container>
+</ngb-tabset>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/org-addr.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/org-addr.component.ts
new file mode 100644 (file)
index 0000000..2092075
--- /dev/null
@@ -0,0 +1,163 @@
+import {Component, Input, Output, EventEmitter} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+
+const ADDR_TYPES =
+    ['billing_address', 'holds_address', 'mailing_address', 'ill_address'];
+
+@Component({
+    selector: 'eg-admin-org-address',
+    templateUrl: './org-addr.component.html'
+})
+export class OrgAddressComponent {
+
+    orgUnit: IdlObject = null;
+    private tabName: string;
+
+    private _orgId: number;
+
+    get orgId(): number { return this._orgId; }
+
+    @Input() set orgId(newId: number) {
+        if (newId) {
+            if (!this._orgId || this._orgId !== newId) {
+                this._orgId = newId;
+                this.init();
+            }
+        } else {
+            this._orgId = null;
+        }
+    }
+
+    @Output() addrChange: EventEmitter<IdlObject>;
+
+    constructor(
+        private idl: IdlService,
+        private org: OrgService,
+        private pcrud: PcrudService
+    ) {
+        this.addrChange = new EventEmitter<IdlObject>();
+        this.tabName = 'billing_address';
+    }
+
+    init() {
+        if (!this.orgId) { return; }
+
+        return this.pcrud.retrieve('aou', this.orgId,
+            {flesh : 1, flesh_fields : {aou : ADDR_TYPES}},
+            {authoritative: true}
+        ).subscribe(org => {
+            this.orgUnit = org;
+            ADDR_TYPES.forEach(aType => {
+                if (!this.addr(aType)) {
+                    this.createAddress(aType);
+                }
+            });
+        });
+    }
+
+    tabChanged($event: NgbTabChangeEvent) {
+        this.tabName = $event.nextId;
+    }
+
+    addrTypes(): string[] { // for UI
+        return ADDR_TYPES;
+    }
+
+    // Template shorthand -- get a specific address by type.
+    addr(addrType: string) {
+        return this.orgUnit ? this.orgUnit[addrType]() : null;
+    }
+
+    createAddress(addrType: string) {
+        const addr = this.idl.create('aoa');
+        addr.isnew(true);
+        addr.valid('t');
+        addr.org_unit(this.orgId);
+        this.orgUnit[addrType](addr);
+    }
+
+    cloneAddress(addrType: string) {
+
+        // Find the address
+        let fromAddr: IdlObject;
+        ADDR_TYPES.forEach(aType => {
+            if (aType !== addrType &&
+                this.addr(aType).id() === this.addr(addrType).id()) {
+                fromAddr = this.addr(aType);
+            }
+        });
+
+        const addr = this.idl.clone(fromAddr);
+        addr.id(null);
+        addr.isnew(true);
+        addr.valid('t');
+        this.orgUnit[addrType](addr);
+    }
+
+    // True if the provided address is used for more than one addr type.
+    sharedAddress(addrId: number): boolean {
+        return ADDR_TYPES.filter(aType => {
+            return (
+                !this.addr(aType).isnew() && this.addr(aType).id() === addrId
+            );
+        }).length > 1;
+    }
+
+    deleteAddress($event: any) {
+        const addr = $event.record;
+        const tmpOrg = this.updatableOrg();
+
+        // Set the FKey to NULL on the org unit for deleted addresses
+        ADDR_TYPES.forEach(aType => {
+            const a = this.addr(aType);
+            if (a && a.id() === addr.id()) {
+                tmpOrg[aType](null);
+                this.createAddress(aType);
+            }
+        });
+
+        this.pcrud.update(tmpOrg).toPromise()
+        .then(_ => this.pcrud.remove(addr).toPromise())
+        .then(_ => this.addrChange.emit(addr));
+    }
+
+    // Addr saved by fm-editor.
+    // In the case of new address creation, point the org unit at
+    // the new address ID.
+    addrSaved(addr: number | IdlObject) {
+
+        if (typeof addr !== 'object') {
+            // pcrud returns a number on 'update' calls.  No need to
+            // reload the data on a simple address change. it's changed
+            // in place.
+            return;
+        }
+
+        // update local copy with version that has an ID.
+        this.orgUnit[this.tabName](addr);
+
+        const org = this.updatableOrg();
+        org[this.tabName](addr.id());
+
+        // Creating a new address -- tell our org about it.
+        this.pcrud.update(org).toPromise().then(_ => this.addrChange.emit(addr));
+    }
+
+    // Create an unfleshed org unit object that's a clone of this.orgUnit
+    // to use when pushing updates to the server.
+    updatableOrg(): IdlObject {
+        const org = this.idl.clone(this.orgUnit);
+
+        ADDR_TYPES.forEach(aType => {
+            const addr = this.addr(aType);
+            if (addr) { org[aType](addr.id()); }
+        });
+
+        return org;
+    }
+
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit-routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit-routing.module.ts
new file mode 100644 (file)
index 0000000..cbd3680
--- /dev/null
@@ -0,0 +1,19 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {OrgUnitComponent} from './org-unit.component';
+
+// Since org-unit admin has its own module with page-level components,
+// it needs its own routing module as well to define which component
+// to display at page load time.
+
+const routes: Routes = [{
+    path: '',
+    component: OrgUnitComponent
+}];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class OrgUnitRoutingModule {}
index 4389217..294d62c 100644 (file)
@@ -14,7 +14,7 @@
 </eg-confirm-dialog>
 
 <ng-template #treeNodeLabelTmpl let-org="org">
-  <span *ngIf="org" i18n>{{org.name()}} ({{org.shortname()}})</span>
+  <span *ngIf="org" i18n>{{org.name()}} -- {{org.shortname()}}</span>
 </ng-template>
 <eg-string #treeNodeLabel key='admin.server.org_unit.treenode' 
   [template]="treeNodeLabelTmpl"></eg-string>
   </div>
   <div class="col-lg-8">
     <div class="alert alert-info">
-      <div *ngIf="selected" i18n>
-        {{currentOrg().name()}} ({{currentOrg().shortname()}})
+      <div *ngIf="currentOrg()">
+        <span *ngIf="currentOrg().name()" i18n>
+          {{currentOrg().name()}} ({{currentOrg().shortname()}})
+        </span>
+        <span *ngIf="!currentOrg().name()" class="font-italic" i18n>
+          Add Name
+        </span>
       </div>
     </div>
-    <ngb-tabset #rootTabs (tabChange)="tabChanged($event)">
+    <ngb-tabset #rootTabs (tabChange)="tabChanged($event)" *ngIf="currentOrg()">
       <ngb-tab title="Main Settings" i18n-title id="main">
         <ng-template ngbTabContent>
           <div class="mt-2">
             <eg-fm-record-editor *ngIf="currentOrg()" #editDialog idlClass="aou" 
               [mode]="currentOrg().isnew() ? 'create': 'update'" [hideBanner]="true" 
               (recordSaved)="orgSaved($event)" displayMode="inline" 
+              (recordDeleted)="orgDeleted()"
               readonlyFields="parent,parent_ou" [preloadLinkedValues]="true"
               [fieldOptions]="{ou_type: {customValues: orgTypeOptions()}}"
               [record]="currentOrg().isnew() ? currentOrg() : null"
               [recordId]="currentOrg().isnew() ? null : currentOrg().id()"
+              [showDelete]="!orgHasChildren()"
               fieldOrder="parent_ou,ou_type,name,shortname,phone,email,opac_visible,fiscal_calendar"
               hiddenFields="id,billing_address,mailing_address,holds_address,ill_address">
               <eg-fm-record-editor-action label="Add Child" i18n-label 
-                [disabled]="orgChildTypes().length === 0"
+                [disabled]="orgChildTypes().length === 0 || currentOrg().isnew()"
+                buttonCss="btn-outline-info"
                 (actionClick)="addChild()"></eg-fm-record-editor-action>
             </eg-fm-record-editor>
           </div>
         </ng-template>
       </ngb-tab>
-      <ngb-tab title="Hours of Operation" i18n-title id="hours">
+      <ngb-tab title="Hours of Operation" i18n-title id="hours" 
+        [disabled]="currentOrg().isnew()">
         <ng-template ngbTabContent>
           <div class="mt-2 common-form striped-even">
             <div class="row font-weight-bold mb-2">
               </div>
             </div>
             <div class="row d-flex justify-content-end">
+              <div class="alert alert-warning mr-2 p-1" 
+                *ngIf="currentOrg().hours_of_operation().isnew()">
+                Hours of Operation Have Not Yet Been Saved.
+              </div>
+              <div class="mr-2">
+                <button class="btn btn-warning" (click)="deleteHours()" i18n>
+                  Clear Hours of Operation
+                </button>
+              </div>
               <div>
                 <button class="btn btn-info" (click)="saveHours()" i18n>
                   Apply Changes
           </div>
         </ng-template>
       </ngb-tab>
-      <ngb-tab title="Addresses" i18n-title id="addresses">
+      <ngb-tab title="Addresses" i18n-title id="addresses" 
+        [disabled]="currentOrg().isnew()">
         <ng-template ngbTabContent>
           <div class="mt-2">
-            <ngb-tabset #addressTabs>
-              <ngb-tab title="Physical Address" i18n-title id="physical">
-                <ng-template ngbTabContent>
-                  <eg-fm-record-editor idlClass="aoa" readonlyFields="org_unit" 
-                    [mode]="currentOrg().billing_address().isnew() ? 'create': 'update'" 
-                    [hideBanner]="true" displayMode="inline" hiddenFields="id"
-                    (recordSaved)="orgSaved()"
-                    [recordId]="currentOrg().billing_address().isnew() ? null : currentOrg().billing_address().id()"
-                    [record]="currentOrg().billing_address().isnew() ? currentOrg().billing_address() : null"
-                    fieldOrder="address_type,street1,street2,city,county,state,country,post_code,san,valid"
-                    >
-                  </eg-fm-record-editor>
-                </ng-template>
-              </ngb-tab>
-              <ngb-tab title="Holds Address" i18n-title id="holds">
-                <ng-template ngbTabContent>
-                  <eg-fm-record-editor idlClass="aoa" readonlyFields="org_unit" 
-                    [mode]="currentOrg().holds_address().isnew() ? 'create': 'update'" 
-                    [hideBanner]="true" displayMode="inline" hiddenFields="id"
-                    (recordSaved)="orgSaved()"
-                    [recordId]="currentOrg().holds_address().isnew() ? null : currentOrg().holds_address().id()"
-                    [record]="currentOrg().holds_address().isnew() ? currentOrg().holds_address() : null"
-                    fieldOrder="address_type,street1,street2,city,county,state,country,post_code,san,valid"
-                    >
-                  </eg-fm-record-editor>
-                </ng-template>
-              </ngb-tab>
-              <ngb-tab title="Mailing Address" i18n-title id="mailing">
-                <ng-template ngbTabContent>
-                  <eg-fm-record-editor idlClass="aoa" readonlyFields="org_unit" 
-                    [mode]="currentOrg().mailing_address().isnew() ? 'create': 'update'" 
-                    [hideBanner]="true" displayMode="inline" hiddenFields="id"
-                    (recordSaved)="orgSaved()"
-                    [recordId]="currentOrg().mailing_address().isnew() ? null : currentOrg().mailing_address().id()"
-                    [record]="currentOrg().mailing_address().isnew() ? currentOrg().mailing_address() : null"
-                    fieldOrder="address_type,street1,street2,city,county,state,country,post_code,san,valid"
-                    >
-                  </eg-fm-record-editor>
-                </ng-template>
-              </ngb-tab>
-              <ngb-tab title="ILL Address" i18n-title id="ill">
-                <ng-template ngbTabContent>
-                  <eg-fm-record-editor idlClass="aoa" readonlyFields="org_unit" 
-                    [mode]="currentOrg().ill_address().isnew() ? 'create': 'update'" 
-                    [hideBanner]="true" displayMode="inline" hiddenFields="id"
-                    (recordSaved)="orgSaved()"
-                    [recordId]="currentOrg().ill_address().isnew() ? null : currentOrg().ill_address().id()"
-                    [record]="currentOrg().ill_address().isnew() ? currentOrg().ill_address() : null"
-                    fieldOrder="address_type,street1,street2,city,county,state,country,post_code,san,valid"
-                    >
-                  </eg-fm-record-editor>
-                </ng-template>
-              </ngb-tab>
-            </ngb-tabset>
+            <eg-admin-org-address [orgId]="currentOrg().id()" (addrChange)="addressChanged($event)">
+            </eg-admin-org-address>
           </div>
         </ng-template>
       </ngb-tab>
index e2ade99..714d7b7 100644 (file)
@@ -42,26 +42,36 @@ export class OrgUnitComponent implements OnInit {
         // stubbing out in case we need it.
     }
 
-    orgSaved(orgId: number) {
-        if (!orgId && this.currentOrg()) {
-            orgId = this.currentOrg().id();
+    orgSaved(orgId: number | IdlObject) {
+        let id;
+
+        if (orgId) { // new org created, focus it.
+            id = typeof orgId === 'object' ? orgId.id() : orgId;
+        } else if (this.currentOrg()) {
+            id = this.currentOrg().id();
         }
-        this.loadAouTree(orgId).then(_ => this.postUpdate(this.editString));
+
+        this.loadAouTree(id).then(_ => this.postUpdate(this.editString));
+    }
+
+    orgDeleted() {
+        this.loadAouTree();
     }
 
     loadAouTree(selectNodeId?: number): Promise<any> {
+
+        const flesh = ['children', 'ou_type', 'hours_of_operation'];
+
         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}
+            {flesh : -1, flesh_fields : {aou : flesh}}, {authoritative: true}
+
         ).toPromise().then(tree => {
             this.ingestAouTree(tree);
-            if (selectNodeId) {
-                const node = this.tree.findNode(selectNodeId);
-                this.selected = node;
-                this.tree.selectNode(node);
-            }
+            if (!selectNodeId) { selectNodeId = this.org.root().id(); }
+
+            const node = this.tree.findNode(selectNodeId);
+            this.selected = node;
+            this.tree.selectNode(node);
         });
     }
 
@@ -75,8 +85,6 @@ export class OrgUnitComponent implements OnInit {
                 this.generateHours(orgNode);
             }
 
-            this.addAddresses(orgNode);
-
             const treeNode = new TreeNode({
                 id: orgNode.id(),
                 label: orgNode.name(),
@@ -103,20 +111,6 @@ export class OrgUnitComponent implements OnInit {
         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());
@@ -175,10 +169,23 @@ export class OrgUnitComponent implements OnInit {
         );
     }
 
+    deleteHours() {
+        const hours = this.currentOrg().hours_of_operation();
+        const promise = hours.isnew() ? Promise.resolve() :
+            this.pcrud.remove(hours).toPromise();
+
+        promise.then(_ => this.generateHours(this.currentOrg()));
+    }
+
     currentOrg(): IdlObject {
         return this.selected ? this.selected.callerData.orgUnit : null;
     }
 
+    orgHasChildren(): boolean {
+        const org = this.currentOrg();
+        return (org && org.children().length > 0);
+    }
+
     postUpdate(message: StringComponent) {
         // Modifying org unit types means refetching the org unit
         // data normally fetched on page load, since it includes
@@ -242,13 +249,14 @@ export class OrgUnitComponent implements OnInit {
 
     addChild() {
         const parentTreeNode = this.selected;
-        const parentOrg = parentTreeNode.callerData.orgUnit;
+        const parentOrg = this.currentOrg();
         const newType = this.orgChildTypes()[0];
 
         const org = this.idl.create('aou');
         org.isnew(true);
         org.parent_ou(parentOrg.id());
         org.ou_type(newType.id());
+        org.children([]);
 
         // Create a dummy, detached org node to keep the UI happy.
         this.selected = new TreeNode({
@@ -257,5 +265,10 @@ export class OrgUnitComponent implements OnInit {
             callerData: {orgUnit: org}
         });
     }
+
+    addressChanged(thing: any) {
+        // Reload to pick up org unit address changes.
+        this.orgSaved(this.currentOrg().id());
+    }
 }
 
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit.module.ts
new file mode 100644 (file)
index 0000000..09150a8
--- /dev/null
@@ -0,0 +1,27 @@
+import {NgModule} from '@angular/core';
+import {TreeModule} from '@eg/share/tree/tree.module';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {OrgUnitComponent} from './org-unit.component';
+import {OrgAddressComponent} from './org-addr.component';
+import {OrgUnitRoutingModule} from './org-unit-routing.module';
+
+@NgModule({
+  declarations: [
+    OrgUnitComponent,
+    OrgAddressComponent
+  ],
+  imports: [
+    AdminCommonModule,
+    OrgUnitRoutingModule,
+    TreeModule
+  ],
+  exports: [
+  ],
+  providers: [
+  ]
+})
+
+export class OrgUnitModule {
+}
+
+
index aa4ec31..3c07655 100644 (file)
@@ -13,7 +13,7 @@ const routes: Routes = [{
     component: OrgUnitTypeComponent
 }, {
     path: 'actor/org_unit',
-    component: OrgUnitComponent
+    loadChildren: '@eg/staff/admin/server/org-unit.module#OrgUnitModule'
 }, {
     path: ':schema/:table',
     component: BasicAdminPageComponent