lp1857911 Statistical Categories UI port
authorMike Risher <mrisher@catalyte.io>
Tue, 31 Dec 2019 19:51:37 +0000 (19:51 +0000)
committerJason Etheridge <jason@EquinoxOLI.org>
Wed, 2 Nov 2022 05:04:53 +0000 (01:04 -0400)
This UI allows editing copy statistical categories (stat cats), patron
statistical categories, and their entries. They are filtered by location.
This port opens a new page when editing deleting or adding entries.

Signed-off-by: Mike Risher <mrisher@catalyte.io>
Signed-off-by: Jason Etheridge <jason@EquinoxOLI.org>
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat_entries.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat_entries.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat_routing.module.ts [new file with mode: 0644]

index a4fb45b..250fbcc 100644 (file)
@@ -7406,7 +7406,10 @@ SELECT  usr,
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
+                               <create permission="CREATE_PATRON_STAT_CAT" context_field="owner"/>
                                <retrieve permission="STAFF_LOGIN" global_required="true"/>
+                               <update permission="UPDATE_PATRON_STAT_CAT" context_field="owner"/>
+                               <delete permission="DELETE_PATRON_STAT_CAT" context_field="owner"/>
                        </actions>
                </permacrud>
        </class>
@@ -7738,7 +7741,7 @@ SELECT  usr,
                        </actions>
                </permacrud>
        </class>
-       <class id="actsce" controller="open-ils.cstore" oils_obj:fieldmapper="actor::stat_cat_entry" oils_persist:tablename="actor.stat_cat_entry" reporter:label="User Stat Cat Entry">
+       <class id="actsce" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::stat_cat_entry" oils_persist:tablename="actor.stat_cat_entry" reporter:label="User Stat Cat Entry">
                <fields oils_persist:primary="id" oils_persist:sequence="actor.stat_cat_entry_id_seq">
                        <field reporter:label="Entry ID" name="id" reporter:datatype="id" />
                        <field reporter:label="Entry Owner" name="owner" reporter:datatype="link"/>
@@ -7751,8 +7754,22 @@ SELECT  usr,
                        <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
                        <link field="default_entries" reltype="has_many" key="stat_cat_entry" map="" class="actsced"/>
                </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+               <actions>
+                               <create permission="CREATE_PATRON_STAT_CAT_ENTRY">
+                                       <context link="stat_cat" field="owner"/>
+                               </create>
+                               <retrieve permission="STAFF_LOGIN" global_required="true"/>
+                               <update permission="UPDATE_PATRON_STAT_CAT_ENTRY">
+                                       <context link="stat_cat" field="owner"/>
+                               </update>
+                               <delete permission="DELETE_PATRON_STAT_CAT_ENTRY">
+                                       <context link="stat_cat" field="owner"/>
+                               </delete>
+                       </actions>
+               </permacrud>
        </class>
-       <class id="actsced" controller="open-ils.cstore" oils_obj:fieldmapper="actor::stat_cat_entry_default" oils_persist:tablename="actor.stat_cat_entry_default" reporter:label="User Stat Cat Default Entry">
+       <class id="actsced" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::stat_cat_entry_default" oils_persist:tablename="actor.stat_cat_entry_default" reporter:label="User Stat Cat Default Entry">
                <fields oils_persist:primary="id" oils_persist:sequence="actor.stat_cat_entry_default_id_seq">
                        <field reporter:label="Default Entry ID" name="id" reporter:datatype="id" />
                        <field reporter:label="Default Entry Value" name="stat_cat_entry" reporter:datatype="link"/>
@@ -7764,6 +7781,20 @@ SELECT  usr,
                        <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
                        <link field="stat_cat_entry" reltype="has_a" key="id" map="" class="actsce"/>
                </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+               <actions>
+                               <create permission="CREATE_PATRON_STAT_CAT_ENTRY">
+                                       <context link="stat_cat" field="owner"/>
+                               </create>
+                               <retrieve permission="STAFF_LOGIN" global_required="true"/>
+                               <update permission="UPDATE_PATRON_STAT_CAT_ENTRY">
+                                       <context link="stat_cat" field="owner"/>
+                               </update>
+                               <delete permission="DELETE_PATRON_STAT_CAT_ENTRY">
+                                       <context link="stat_cat" field="owner"/>
+                               </delete>
+                       </actions>
+               </permacrud>
        </class>
        <class id="cubi" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="container::user_bucket_item" oils_persist:tablename="container.user_bucket_item" reporter:label="User Bucket Item">
                <fields oils_persist:primary="id" oils_persist:sequence="container.user_bucket_item_id_seq">
@@ -8872,7 +8903,16 @@ SELECT  usr,
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
+                               <create permission="CREATE_PATRON_STAT_CAT_ENTRY">
+                                       <context link="stat_cat" field="owner"/>
+                               </create>
                                <retrieve permission="STAFF_LOGIN" global_required="true"/>
+                               <update permission="UPDATE_PATRON_STAT_CAT_ENTRY">
+                                       <context link="stat_cat" field="owner"/>
+                               </update>
+                               <delete permission="DELETE_PATRON_STAT_CAT_ENTRY">
+                                       <context link="stat_cat" field="owner"/>
+                               </delete>
                        </actions>
                </permacrud>
        </class>
index 92d22f0..37e3377 100644 (file)
@@ -71,7 +71,7 @@
     <eg-link-table-link i18n-label label="Standing Penalties" 
       routerLink="/staff/admin/local/config/standing_penalty"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Statistical Categories Editor" 
-      url="/eg/staff/admin/local/asset/stat_cat_editor"></eg-link-table-link>
+      routerLink="/staff/admin/local/asset/stat_cat_editor"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Statistical Popularity Badges" 
       routerLink="/staff/admin/local/rating/badge"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Surveys" 
index 267f008..c4fc5a9 100644 (file)
@@ -19,6 +19,10 @@ const routes: Routes = [{
     path: 'actor/address_alert',
     component: AddressAlertComponent
 }, {
+    path: 'asset/stat_cat_editor',
+    loadChildren: () =>
+        import('./stat_cat/stat_cat.module').then(m => m.StatCatModule)
+}, {
     path: 'asset/copy_location_order',
     loadChildren: () =>
       import('./copy-loc-order/copy-loc-order.module').then(m => m.CopyLocOrderModule)
@@ -34,6 +38,10 @@ const routes: Routes = [{
     loadChildren: () =>
       import('./shelving_location_groups/shelving_location_groups.module').then(m => m.ShelvingLocationGroupsModule)
 }, {
+    path: 'asset/stat_cat_editor/:tab',
+    loadChildren: () =>
+        import('./stat_cat/stat_cat.module').then(m => m.StatCatModule)
+}, {
     path: 'container/carousel',
     component: AdminCarouselComponent
 }, {
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat.component.html
new file mode 100644 (file)
index 0000000..1c87090
--- /dev/null
@@ -0,0 +1,86 @@
+<eg-staff-banner bannerText="Statistical Category Editor" i18n-bannerText></eg-staff-banner>
+<div class="row">
+    <div class="col-lg-12">        
+        <ngb-tabset #scTabs [activeId]="currentTab"  (tabChange)="onTabChange($event)">    
+            <ngb-tab title="Copy Statistical Categories" i18n-title id="'copy'">
+                <ng-template ngbTabContent>
+                    <eg-fm-record-editor #copyDialog idlClass="asc"
+                        preloadLinkedValues="true" requiredFields="name,owner"
+                        readonlyFields="owner"
+                        hiddenFieldsList="id"></eg-fm-record-editor> 
+                    <div class="col-lg-2 offset-lg-5 mt-3 mb-3">
+                        <div class="text-center">Focus location</div>
+                        <eg-org-select i18n-placeholder #focus
+                            (onChange)="orgOnChange($event, 'copy')" 
+                            [hideOrgs]="disableCopyOrgs()"
+                            [initialOrg]="selectedCopyOrg"></eg-org-select>
+                    </div>
+                    <eg-grid #copyGrid idlClass="asc" [dataSource]="ascDataSource"
+                        hideFields="id" (onRowActivate)="editStatCat([$event])">
+                        <eg-grid-toolbar-button label="New Copy Statistical Category" 
+                            i18n-label [action]="newStatCat" 
+                            [disabled]="!userPerms.canCreateCopy"></eg-grid-toolbar-button>
+                        <eg-grid-toolbar-action label="Edit Selected" i18n-label 
+                            [action]="editStatCat"></eg-grid-toolbar-action>
+                        <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+                            [action]="deleteStatCat"></eg-grid-toolbar-action>
+                        <ng-template #copyEntriesTmpl let-row="row">
+                            <a href="staff/admin/local/asset/stat_cat_editor/copy_entries/{{row.id()}}">
+                                Edit Entries
+                            </a>
+                        </ng-template>
+                        <eg-grid-column i18n-label label="Entries"
+                            [cellTemplate]="copyEntriesTmpl"></eg-grid-column>
+                    </eg-grid>
+                </ng-template>
+            </ngb-tab>
+            <ngb-tab title="Patron Statistical Categories" i18n-title id="'patron'">
+                <ng-template ngbTabContent>
+                    <eg-fm-record-editor #patronDialog idlClass="actsc"
+                        preloadLinkedValues="true" requiredFields="name,owner"
+                        readonlyFields="owner"
+                        hiddenFieldsList="id"></eg-fm-record-editor>
+                    <div class="col-lg-2 offset-lg-5 mt-3 mb-3">
+                        <div class="text-center">Focus location</div>
+                        <eg-org-select i18n-placeholder #focus 
+                            (onChange)="orgOnChange($event, 'patron')"
+                            [hideOrgs]="disablePatronOrgs()"
+                            [initialOrg]="selectedPatronOrg"></eg-org-select>
+                    </div>
+                    <eg-grid #patronGrid idlClass="actsc" [dataSource]="actscDataSource"
+                        (onRowActivate)="editStatCat([$event])" hideFields="id">
+                        <eg-grid-toolbar-button label="New Patron Statistical Category" 
+                            i18n-label [action]="newStatCat"
+                            [disabled]="!userPerms.canCreatePatron"></eg-grid-toolbar-button>
+                        <eg-grid-toolbar-action label="Edit Selected" i18n-label 
+                            [action]="editStatCat"></eg-grid-toolbar-action>
+                        <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+                            [action]="deleteStatCat"></eg-grid-toolbar-action>
+                        <ng-template #patronEntriesTmpl let-row="row">
+                            <a href="staff/admin/local/asset/stat_cat_editor/patron_entries/{{row.id()}}">
+                                Edit Entries
+                            </a>
+                        </ng-template>
+                        <eg-grid-column i18n-label label="Entries"
+                            [cellTemplate]="patronEntriesTmpl"></eg-grid-column>
+                    </eg-grid>
+                </ng-template>
+            </ngb-tab>
+        </ngb-tabset>
+    </div>    
+</div>
+
+<eg-confirm-dialog #confirmDialog
+  i18n-dialogTitle i18n-dialogBody
+  dialogTitle="Warning"
+  dialogBody="One or more copy stat cats are currently in use.  Delete all anyway?">
+</eg-confirm-dialog>
+
+<eg-string #createSuccessString i18n-text text="New record created"></eg-string>
+<eg-string #createErrString i18n-text text="Failed to create new record"></eg-string>
+<eg-string #deleteFailedString i18n-text text="Deletion failed or was not allowed">
+</eg-string>
+<eg-string #deleteSuccessString i18n-text text="Deletion succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Update failed or was not allowed">
+</eg-string>
+<eg-string #updateSuccessString i18n-text text="Update succeeded"></eg-string>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat.component.ts
new file mode 100644 (file)
index 0000000..ad62347
--- /dev/null
@@ -0,0 +1,326 @@
+import {Pager} from '@eg/share/util/pager';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {OrgService} from '@eg/core/org.service';
+import {PermService} from '@eg/core/perm.service';
+import {AuthService} from '@eg/core/auth.service';
+
+@Component({
+    templateUrl: './stat_cat.component.html'
+})
+
+export class StatCatComponent implements OnInit {
+
+    currentTab: String;
+    ascDataSource: GridDataSource = new GridDataSource();
+    actscDataSource: GridDataSource = new GridDataSource();
+    selectedCopyOrg: IdlObject = this.org.get(this.auth.user().ws_ou());
+    selectedPatronOrg: IdlObject = this.org.get(this.auth.user().ws_ou());
+    selectedCopyOrgId: number = this.org.get(this.auth.user().ws_ou()).id();
+    selectedPatronOrgId: number = this.org.get(this.auth.user().ws_ou()).id();
+
+    // list of orgs to retrieve stat cats for
+    copyOrgFamily: any;
+    patronOrgFamily: any;
+
+    // object which has all permissions data for current user
+    userPerms: any = {};
+
+    // orgs that the user doesn't have permission to view.
+    hiddenCopyOrgs: any[];
+    hiddenPatronOrgs: any[];
+
+    newActsc: IdlObject;
+    newAsc: IdlObject;
+
+    @ViewChild('copyGrid') copyGrid: GridComponent;
+    @ViewChild('patronGrid') patronGrid: GridComponent;
+    @ViewChild('copyDialog') copyDialog: FmRecordEditorComponent;
+    @ViewChild('patronDialog') patronDialog: FmRecordEditorComponent;
+    @ViewChild('updateSuccessString') updateSuccessString: StringComponent;
+    @ViewChild('updateFailedString') updateFailedString: StringComponent;
+    @ViewChild('deleteFailedString') deleteFailedString: StringComponent;
+    @ViewChild('deleteSuccessString') deleteSuccessString: StringComponent;
+    @ViewChild('createSuccessString') createSuccessString: StringComponent;
+    @ViewChild('createErrString') createErrString: StringComponent;
+
+    constructor(
+        private pcrud: PcrudService,
+        private router: Router,
+        private route: ActivatedRoute,
+        private org: OrgService,
+        private idl: IdlService,
+        private toast: ToastService,
+        private perm: PermService,
+        private auth: AuthService,
+    ) {
+    }
+
+    ngOnInit() {
+        this.newActsc = this.idl.create('actsc');
+        this.newAsc = this.idl.create('asc');
+        this.perm.hasWorkPermAt(['CREATE_COPY_STAT_CAT', 'CREATE_PATRON_STAT_CAT',
+            'UPDATE_PATRON_STAT_CAT', 'UPDATE_COPY_STAT_CAT', 'DELETE_COPY_STAT_CAT',
+            'DELETE_PATRON_STAT_CAT'], true)
+            .then(userPerms => {
+                this.userPerms = userPerms;
+                const allOrgs = this.org.list();
+
+                // View permission is the same as permission to update,
+                // which is how it was handled in the old UI.
+                this.hiddenCopyOrgs = allOrgs.filter(x => {
+                    return !this.userPerms.UPDATE_COPY_STAT_CAT.includes(x.id());
+                }).map(x => {
+                    return x.id();
+                });
+                this.hiddenPatronOrgs = allOrgs.filter(x => {
+                    return !this.userPerms.UPDATE_PATRON_STAT_CAT.includes(x.id());
+                }).map(x => {
+                    return x.id();
+                });
+
+                if (this.route.snapshot.paramMap.get('tab') === 'patron') {
+                    this.onTabChange({nextId: '\'patron\''});
+                } else { // if null or 'copy'
+                    this.onTabChange({nextId: '\'copy\''});
+                }
+        });
+    }
+
+    disableCopyOrgs = () => this.org.filterList( { inList: this.hiddenCopyOrgs }, true);
+
+    disablePatronOrgs = () => this.org.filterList( { inList: this.hiddenCopyOrgs }, true);
+
+    setCurrentPerms() {
+        this.userPerms.canEditCopy =
+            this.userPerms.UPDATE_COPY_STAT_CAT.indexOf(this.selectedCopyOrgId) !== -1;
+        this.userPerms.canEditPatron =
+            this.userPerms.UPDATE_PATRON_STAT_CAT.indexOf(this.selectedPatronOrgId) !== -1;
+        this.userPerms.canCreateCopy =
+            this.userPerms.CREATE_COPY_STAT_CAT.indexOf(this.selectedCopyOrgId) !== -1;
+        this.userPerms.canCreatePatron =
+            this.userPerms.CREATE_PATRON_STAT_CAT.indexOf(this.selectedPatronOrgId) !== -1;
+        this.userPerms.canDeleteCopy =
+            this.userPerms.DELETE_COPY_STAT_CAT.indexOf(this.selectedCopyOrgId) !== -1;
+        this.userPerms.canDeletePatron =
+            this.userPerms.DELETE_PATRON_STAT_CAT.indexOf(this.selectedPatronOrgId) !== -1;
+    }
+
+    onTabChange(event: any) {
+        this.currentTab = event.nextId;
+        if (this.currentTab === '\'patron\'') {
+            this.routeToTab('patron');
+            this.orgOnChange(this.selectedPatronOrg, 'patron');
+        } else {
+            this.routeToTab('copy');
+            this.orgOnChange(this.selectedCopyOrg, 'copy');
+        }
+    }
+
+    routeToTab(tab) {
+        const url = `/staff/admin/local/asset/stat_cat_editor/${tab}`;
+        this.router.navigate([url]);
+    }
+
+    orgOnChange = (org: IdlObject, type): void => {
+        this.setCurrentPerms();
+        if (type === 'copy') {
+            this.selectedCopyOrg = org;
+            this.selectedCopyOrgId = org.id();
+            this.getOrgFamilyAndData('copy');
+            if (this.copyGrid) {
+                this.copyGrid.reload();
+            }
+        } else {
+            this.selectedPatronOrg = org;
+            this.selectedPatronOrgId = org.id();
+            this.getOrgFamilyAndData('patron');
+            if (this.patronGrid) {
+                this.patronGrid.reload();
+            }
+        }
+    }
+
+    getOrgFamilyAndData(type) {
+        if (type === 'copy') {
+            this.copyOrgFamily = this.org.fullPath(this.selectedCopyOrgId)
+            .map(record => {
+                return record.id();
+            });
+            this.getAscData();
+        } else {
+            this.patronOrgFamily = this.org.fullPath(this.selectedPatronOrgId)
+            .map(record => {
+                return record.id();
+            });
+            this.getActscData();
+        }
+    }
+
+    getAscData() {
+        this.ascDataSource.getRows = (pager: Pager, sort: any[]) => {
+            const orderBy = {asc: 'id'};
+            return this.pcrud.search('asc', {owner: this.copyOrgFamily}, orderBy);
+        };
+    }
+
+    getActscData() {
+        this.actscDataSource.getRows = (pager: Pager, sort: any[]) => {
+            const orderBy = {actsc: 'id'};
+            return this.pcrud.search('actsc', {owner: this.patronOrgFamily}, orderBy);
+        };
+    }
+
+    editStatCat = (idlThing) => {
+        const idlString = idlThing[0].classname;
+
+        // automatically fail if no permissions
+        if ((!this.userPerms.canEditCopy && idlString === 'asc') ||
+            (!this.userPerms.canEditPatron && idlString === 'actsc')) {
+            this.updateFailedString.current()
+                .then(str => this.toast.danger(str));
+            return;
+        }
+
+        const lookupResponse = this.lookUpType(idlString);
+        const currentGrid = lookupResponse.currentGrid;
+        const currentDialog = lookupResponse.currentDialog;
+        currentDialog.mode = 'update';
+        currentDialog.recordId = idlThing[0].id();
+        currentDialog.open({size: 'lg'}).subscribe(
+            id => {
+                console.debug('Record editor performed action');
+                currentGrid.reload();
+            },
+            err => {
+                console.debug(err);
+            },
+            () => console.debug('Dialog closed')
+        );
+    }
+
+    deleteStatCat = (idlThings: IdlObject[]) => {
+        const idlString = idlThings[0].classname;
+
+        // automatically fail if no permissions
+        if ((!this.userPerms.canDeleteCopy && idlString === 'asc') ||
+            (!this.userPerms.canDeletePatron && idlString === 'actsc')) {
+            this.deleteFailedString.current()
+                .then(str => this.toast.danger(str));
+            return;
+        }
+
+        // check to see if copy stat cat is in use
+        if (idlString === 'asc') {
+            this.checkThenDelete(idlThings, idlString);
+        } else {
+            this.doDelete(idlThings, idlString);
+        }
+    }
+
+    checkThenDelete = (idlThings, idlString) => {
+        const promises: any[] = [];
+        idlThings.forEach(idlThing => { // check one at a time to see if it's used
+            const id = idlThing.id();
+            const promise: any = new Promise((resolve, reject) => {
+                this.pcrud.search('ascecm', {stat_cat: id}, {limit: 1})
+                    .toPromise().then(
+                    rec => {
+                        if (rec) {
+                            this.deleteFailedString.current()
+                                .then(str => this.toast.danger(str));
+                            resolve(false);
+                        } else {
+                            resolve(true);
+                        }
+                    });
+            });
+            promises.push(promise);
+        });
+        const result = Promise.all(promises);
+        result.then(responses => {
+            if (!responses.includes(false)) {  // only delete if none in use
+                this.doDelete(idlThings, idlString);
+            }
+        });
+    }
+
+    doDelete = (idlThings: IdlObject[], idlString) => {
+        idlThings.forEach(idlThing => {
+            idlThing.isdeleted(true);
+        });
+        const currentGrid = this.lookUpType(idlString).currentGrid;
+        this.pcrud.autoApply(idlThings).subscribe(
+            val => {
+                console.debug('deleted: ' + val);
+                this.deleteSuccessString.current()
+                    .then(str => this.toast.success(str));
+            },
+            err => {
+                this.deleteFailedString.current()
+                    .then(str => this.toast.danger(str));
+            },
+            () => {
+                currentGrid.reload();
+            }
+        );
+    }
+
+    lookUpType = (idlString) => {
+        let currentDialog;
+        let currentGrid;
+        if (idlString === 'asc') {
+            currentGrid = this.copyGrid;
+            currentDialog = this.copyDialog;
+        } else {
+            currentGrid = this.patronGrid;
+            currentDialog = this.patronDialog;
+        }
+        return {
+            currentDialog: currentDialog,
+            currentGrid: currentGrid
+        };
+    }
+
+    newStatCat = (type: any) => {
+        let currentDialog;
+        let currentGrid;
+        let newIdlObject;
+        if (this.currentTab === '\'patron\'') {
+            currentGrid = this.patronGrid;
+            currentDialog = this.patronDialog;
+            newIdlObject = this.idl.clone(this.newActsc);
+            newIdlObject.owner(this.selectedPatronOrg);
+        } else {
+            currentGrid = this.copyGrid;
+            currentDialog = this.copyDialog;
+            newIdlObject = this.idl.clone(this.newAsc);
+            newIdlObject.owner(this.selectedCopyOrg);
+        }
+        currentDialog.mode = 'create';
+        currentDialog.recordId = null;
+        currentDialog.record = newIdlObject;
+        currentDialog.open({size: 'lg'}).subscribe(
+            ok => {
+                this.createSuccessString.current()
+                    .then(str => this.toast.success(str));
+                currentGrid.reload();
+            },
+            rejection => {
+                if (!rejection.dismissed) {
+                    this.createErrString.current()
+                        .then(str => this.toast.danger(str));
+                }
+            }
+        );
+    }
+
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat.module.ts
new file mode 100644 (file)
index 0000000..6be786b
--- /dev/null
@@ -0,0 +1,20 @@
+import {NgModule} from '@angular/core';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {StatCatComponent} from './stat_cat.component';
+import {StatCatEntriesComponent} from './stat_cat_entries.component';
+import {StatCatRoutingModule} from './stat_cat_routing.module';
+
+@NgModule({
+  declarations: [
+    StatCatComponent,
+    StatCatEntriesComponent,
+  ],
+  imports: [
+    AdminCommonModule,
+    StatCatRoutingModule,
+  ],
+  exports: [],
+  providers: []
+})
+
+export class StatCatModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat_entries.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat_entries.component.html
new file mode 100644 (file)
index 0000000..431f8ef
--- /dev/null
@@ -0,0 +1,56 @@
+<eg-staff-banner bannerText="{{statCatType}} Statistical Category Entry Editor" 
+    i18n-bannerText></eg-staff-banner>
+
+<ng-template #orgTemplate>
+    <eg-org-select [hideOrgs]="hiddenOrgs" (onChange)="setOwner($event)" 
+        [initialOrgId]="statCatOrgId"></eg-org-select>
+</ng-template>
+
+<eg-fm-record-editor #patronDialog idlClass="actsce" 
+    [fieldOptions]="{owner: {customTemplate: {template: orgTemplate}}}"
+    readonlyFieldsList="stat_cat"
+    requiredFields="value,owner" hiddenFieldsList="id"></eg-fm-record-editor>
+<eg-fm-record-editor #copyDialog idlClass="asce" 
+    [fieldOptions]="{owner: {customTemplate: {template: orgTemplate}}}"
+    readonlyFieldsList="stat_cat"
+    requiredFields="value,owner" hiddenFieldsList="id"></eg-fm-record-editor>
+
+<div class="row justify-content-center">
+    <button class="btn btn-outline-dark mb-1 text-center" (click)="back()">
+        &#8592; Back to Stat Cats
+    </button>
+</div>
+    
+<div *ngIf="statCatType === 'Copy'">
+    <eg-grid #copyGrid [dataSource]="copyDataSource" idlClass="asce"
+        (onRowActivate)="editEntry([$event])" hideFields="id,stat_cat">
+        <eg-grid-toolbar-button label="New Entry" i18n-label
+            [action]="createNewEntry" 
+            [disabled]="!userPerms.canCreate"></eg-grid-toolbar-button>
+        <eg-grid-toolbar-action label="Edit Selected" i18n-label 
+            [action]="editEntry"></eg-grid-toolbar-action>
+        <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+            [action]="deleteEntry"></eg-grid-toolbar-action>
+    </eg-grid>
+</div>
+<div *ngIf="statCatType === 'Patron'">
+    <eg-grid #patronGrid [dataSource]="patronDataSource" idlClass="actsce"
+        (onRowActivate)="editEntry([$event])" hideFields="id,stat_cat">
+        <eg-grid-toolbar-button label="New Entry" i18n-label
+            [action]="createNewEntry" 
+            [disabled]="!userPerms.canCreate"></eg-grid-toolbar-button>
+        <eg-grid-toolbar-action label="Edit Selected" i18n-label 
+            [action]="editEntry"></eg-grid-toolbar-action>
+        <eg-grid-toolbar-action label="Delete Selected" i18n-label 
+            [action]="deleteEntry"></eg-grid-toolbar-action>
+    </eg-grid>
+</div>
+
+<eg-string #createSuccessString i18n-text text="New entry Added"></eg-string>
+<eg-string #createErrString i18n-text text="Failed to create new entry"></eg-string>
+<eg-string #deleteFailedString i18n-text text="Deletion failed or was not allowed">
+</eg-string>
+<eg-string #deleteSuccessString i18n-text text="Deletion succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Update failed or was not allowed">
+</eg-string>
+<eg-string #updateSuccessString i18n-text text="Update succeeded"></eg-string>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat_entries.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat_entries.component.ts
new file mode 100644 (file)
index 0000000..19a0f15
--- /dev/null
@@ -0,0 +1,235 @@
+import {Pager} from '@eg/share/util/pager';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {Router} from '@angular/router';
+import {PermService} from '@eg/core/perm.service';
+
+@Component({
+    templateUrl: './stat_cat_entries.component.html'
+})
+
+export class StatCatEntriesComponent implements OnInit {
+
+    statCatId: number; // parent stat cat id
+    statCatOrgId: number; // parent stat cat org id
+    statCatOrg: IdlObject; // parent stat cat org Idl
+    hiddenOrgs: any[];  // List of invalid orgs for a new entry
+    statCatType: 'Copy' | 'Patron';  // parent type
+    defaultNewRecord: IdlObject;
+    copyDataSource: GridDataSource = new GridDataSource();
+    patronDataSource: GridDataSource = new GridDataSource();
+    userPerms: any = {};
+
+    @ViewChild('copyGrid', {static: false}) copyGrid: GridComponent;
+    @ViewChild('patronGrid', {static: false}) patronGrid: GridComponent;
+    @ViewChild('copyDialog', {static: false}) copyDialog: FmRecordEditorComponent;
+    @ViewChild('patronDialog', {static: false}) patronDialog: FmRecordEditorComponent;
+    @ViewChild('updateSuccessString', {static: false}) updateSuccessString: StringComponent;
+    @ViewChild('updateFailedString', {static: false}) updateFailedString: StringComponent;
+    @ViewChild('deleteFailedString', {static: false}) deleteFailedString: StringComponent;
+    @ViewChild('deleteSuccessString', {static: false}) deleteSuccessString: StringComponent;
+    @ViewChild('createSuccessString', {static: false}) createSuccessString: StringComponent;
+    @ViewChild('createErrString', {static: false}) createErrString: StringComponent;
+
+    constructor(
+        private router: Router,
+        private idl: IdlService,
+        private pcrud: PcrudService,
+        private route: ActivatedRoute,
+        private toast: ToastService,
+        private perm: PermService,
+        private org: OrgService
+    ) {
+    }
+
+    ngOnInit() {
+        this.statCatId = parseInt(this.route.snapshot.paramMap.get('id'), 10);
+        if (this.router.url.includes('copy_entries')) {
+            this.statCatType = 'Copy';
+            this.copyDataSource.getRows = (pager: Pager, sort: any[]) => {
+                const orderBy: any = {order_by: {asce: 'id'}};
+                return this.pcrud.search('asce', {stat_cat: this.statCatId}, orderBy);
+            };
+            this.getParentData('asc');
+        } else if (this.router.url.includes('patron_entries')) {
+            this.statCatType = 'Patron';
+            this.patronDataSource.getRows = (pager: Pager, sort: any[]) => {
+                const orderBy: any = {order_by: {actsce: 'id'}};
+                return this.pcrud.search('actsce', {stat_cat: this.statCatId}, orderBy);
+            };
+            this.getParentData('actsc');
+        } else {
+            console.debug('Error - unable to determine type of stat cat');
+        }
+    }
+
+    back = () => {
+        let tab = 'copy';
+        if (this.statCatType === 'Patron') {
+            tab = 'patron';
+        }
+        this.router.navigate(['/staff/admin/local/asset/stat_cat_editor/' + tab]);
+    }
+
+    checkPerms = () => {
+        this.perm.hasWorkPermAt(['CREATE_COPY_STAT_CAT_ENTRY',
+            'CREATE_PATRON_STAT_CAT_ENTRY', 'DELETE_COPY_STAT_CAT_ENTRY',
+            'DELETE_PATRON_STAT_CAT_ENTRY', 'UPDATE_COPY_STAT_CAT_ENTRY',
+            'UPDATE_PATRON_STAT_CAT_ENTRY'], true)
+            .then(userPerms => {
+            this.userPerms = userPerms;
+            if (this.statCatType === 'Copy') {
+                this.userPerms.canEdit =
+                    this.userPerms.UPDATE_COPY_STAT_CAT_ENTRY.indexOf(this.statCatOrg) !== -1;
+                this.userPerms.canCreate =
+                    this.userPerms.CREATE_COPY_STAT_CAT_ENTRY.indexOf(this.statCatOrg) !== -1;
+                this.userPerms.canDelete =
+                    this.userPerms.DELETE_COPY_STAT_CAT_ENTRY.indexOf(this.statCatOrg) !== -1;
+            } else {
+                this.userPerms.canEdit =
+                    this.userPerms.UPDATE_PATRON_STAT_CAT_ENTRY.indexOf(this.statCatOrg) !== -1;
+                this.userPerms.canCreate =
+                    this.userPerms.CREATE_PATRON_STAT_CAT_ENTRY.indexOf(this.statCatOrg) !== -1;
+                this.userPerms.canDelete =
+                    this.userPerms.DELETE_PATRON_STAT_CAT_ENTRY.indexOf(this.statCatOrg) !== -1;
+            }
+        });
+    }
+
+    getParentData = (searchIdl) => {
+        return this.pcrud.search(searchIdl, {id: this.statCatId}, {}
+        ).subscribe(rec => {
+            this.statCatOrg = rec.owner();
+            this.statCatOrgId = rec.owner();
+            this.checkPerms();
+            const allOrgIds = this.org.list().map(x => x.id());
+            const statCatDescendantIds = this.org.descendants(rec.owner(), true);
+            this.hiddenOrgs = allOrgIds.filter(org => {
+                return !statCatDescendantIds.includes(org);
+            });
+            return rec;
+        });
+    }
+
+    setOwner(e): void {
+        this.defaultNewRecord.owner(e);
+    }
+
+    lookupType() {
+        let currentDialog;
+        let currentGrid;
+        let newIdl;
+        if (this.statCatType === 'Copy') {
+            currentGrid = this.copyGrid;
+            currentDialog = this.copyDialog;
+            newIdl = 'asce';
+        } else {
+            currentGrid = this.patronGrid;
+            currentDialog = this.patronDialog;
+            newIdl = 'actsce';
+        }
+        return {
+            currentGrid: currentGrid,
+            currentDialog: currentDialog,
+            newIdl: newIdl
+        };
+    }
+
+    createNewEntry = () => {
+        // automatically fail if no permissions
+        if (!this.userPerms.canCreate) {
+            this.createErrString.current()
+                .then(str => this.toast.danger(str));
+            return;
+        }
+
+        const lookupResponse = this.lookupType();
+        const currentDialog = lookupResponse.currentDialog;
+        const currentGrid = lookupResponse.currentGrid;
+        this.defaultNewRecord = this.idl.create(lookupResponse.newIdl);
+
+        // pre-populate new record with the current stat_cat and org
+        this.defaultNewRecord.owner(this.statCatOrg);
+        this.defaultNewRecord.stat_cat(this.statCatId);
+        currentDialog.mode = 'create';
+        currentDialog.recordId = null;
+        currentDialog.record = this.defaultNewRecord;
+        currentDialog.open({size: 'lg'}).subscribe(
+            ok => {
+                this.createSuccessString.current()
+                    .then(str => this.toast.success(str));
+                currentGrid.reload();
+            },
+            rejection => {
+                if (!rejection.dismissed) {
+                    this.createErrString.current()
+                        .then(str => this.toast.danger(str));
+                }
+            }
+        );
+    }
+
+    editEntry = (idlThing) => {
+        // automatically fail if no permissions
+        if (!this.userPerms.canEdit) {
+            this.updateFailedString.current()
+                .then(str => this.toast.danger(str));
+            return;
+        }
+
+        const lookupResponse = this.lookupType();
+        const currentDialog = lookupResponse.currentDialog;
+        const currentGrid = lookupResponse.currentGrid;
+        currentDialog.mode = 'update';
+        currentDialog.recordId = idlThing[0].id();
+        currentDialog.open({size: 'lg'}).subscribe(
+            id => {
+                this.updateSuccessString.current()
+                    .then(str => this.toast.success(str));
+                console.debug('Record editor performed action');
+                currentGrid.reload();
+            },
+            err => {
+                console.debug(err);
+                this.updateFailedString.current()
+                    .then(str => this.toast.danger(str));
+            },
+            () => console.debug('Dialog closed')
+        );
+    }
+
+    deleteEntry = (idlThings: IdlObject[]) => {
+        // automatically fail if no permissions
+        if (!this.userPerms.canDelete) {
+            this.deleteFailedString.current()
+                .then(str => this.toast.danger(str));
+            return;
+        }
+
+        const currentGrid = this.lookupType().currentGrid;
+        idlThings.forEach(idlThing => idlThing.isdeleted(true));
+        this.pcrud.autoApply(idlThings).subscribe(
+            val => {
+                console.debug('deleted: ' + val);
+                this.deleteSuccessString.current()
+                    .then(str => this.toast.success(str));
+            },
+            err => {
+                this.deleteFailedString.current()
+                    .then(str => this.toast.danger(str));
+            },
+            () => {
+                currentGrid.reload();
+            }
+        );
+    }
+
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat_routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/stat_cat/stat_cat_routing.module.ts
new file mode 100644 (file)
index 0000000..f2d205d
--- /dev/null
@@ -0,0 +1,22 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {StatCatComponent} from './stat_cat.component';
+import {StatCatEntriesComponent} from './stat_cat_entries.component';
+
+const routes: Routes = [{
+  path: '',
+  component: StatCatComponent
+}, {
+  path: 'copy_entries/:id',
+  component: StatCatEntriesComponent
+}, {
+  path: 'patron_entries/:id',
+  component: StatCatEntriesComponent
+}];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class StatCatRoutingModule {}