LP1904036 juve flag; perm groups editing, phone pw, misc.
authorBill Erickson <berickxx@gmail.com>
Tue, 30 Mar 2021 21:19:42 +0000 (17:19 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:29 +0000 (20:13 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/statcats.component.ts

index 531cc4b..0087916 100644 (file)
@@ -20,7 +20,7 @@
 <eg-confirm-dialog #adjustToZeroDialog *ngIf="billGrid"
   i18n-dialogBody i18n-dialogTitle dialogTitle="Adjust To Zero"
   dialogBody="Are you sure you would like to adjust to zero the balance 
-    on transactoins {{billGrid.context.rowSelector.selected().join(',')}}?">
+    on transactions {{billGrid.context.rowSelector.selected().join(',')}}?">
 </eg-confirm-dialog>
 
 <eg-confirm-dialog #refundDialog *ngIf="billGrid"
         <div class="flex-4" i18n>Total Owed:</div>
         <div class="flex-1"
           [ngClass]="{'font-weight-bold' : summary.balance_owed() > 0}">
-          {{summary.balance_owed() | currency}}</div>
+          {{summary.balance_owed() || 0 | currency}}</div>
       </div>
       <div class="d-flex pt-1 pb-1">
         <div class="flex-4" i18n>Total Billed:</div>
-        <div class="flex-1">{{summary.total_owed() | currency}}</div>
+        <div class="flex-1">{{summary.total_owed() || 0 | currency}}</div>
       </div>
       <div class="d-flex pt-1 pb-1 striped">
         <div class="flex-4" i18n>Total Paid/Credited:</div>
-        <div class="flex-1">{{summary.total_paid() | currency}}</div>
+        <div class="flex-1">{{summary.total_paid() || 0 | currency}}</div>
       </div>
     </div>
 
index a87bc67..23223c0 100644 (file)
@@ -3,7 +3,7 @@ import {Router, ActivatedRoute, ParamMap} from '@angular/router';
 import {from, empty, range} from 'rxjs';
 import {concatMap, tap, takeLast} from 'rxjs/operators';
 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
-import {IdlObject} from '@eg/core/idl.service';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
 import {EventService} from '@eg/core/event.service';
 import {OrgService} from '@eg/core/org.service';
 import {NetService} from '@eg/core/net.service';
@@ -78,6 +78,7 @@ export class BillsComponent implements OnInit, AfterViewInit {
         private net: NetService,
         private pcrud: PcrudService,
         private auth: AuthService,
+        private idl: IdlService,
         private printer: PrintService,
         private serverStore: ServerStoreService,
         private circ: CircService,
@@ -200,6 +201,13 @@ export class BillsComponent implements OnInit, AfterViewInit {
         })).toPromise()
 
         .then(_ => {
+
+            if (!this.summary) {
+                // If the patron has no billing history, there will be
+                // no money summary.
+                this.summary = this.idl.create('mus');
+            }
+
             if (!refreshXacts) { return; }
 
             // Refreshing means some transactions may be removed from the list
index bcd8ba5..e052f0c 100644 (file)
   i18n-dialogTitle dialogTitle="Address Alert">
 </eg-alert-dialog>
 
+<eg-alert-dialog #addrRequiredAlert
+  i18n-dialogTitle dialogTitle="Address Required"
+  i18n-dialogBody dialogBody="An address is required during registration">
+</eg-alert-dialog>
+
+
 <div class="row" *ngIf="loading">
   <div class="col-lg-6 offset-lg-3">
     <eg-progress-inline></eg-progress-inline>
   </div>
 </ng-template>
 
-<ng-template #userSettingsInput let-args="args">
+<ng-template #userSettingInput let-args="args">
   <div class="col-lg-3">
     <input 
       type="{{args.type || 'text'}}"
index ab855bb..7471da3 100644 (file)
@@ -123,6 +123,8 @@ export class EditComponent implements OnInit, AfterViewInit {
     @ViewChild('secondaryGroupsDialog')
         private secondaryGroupsDialog: SecondaryGroupsDialogComponent;
     @ViewChild('addrAlert') private addrAlert: AlertDialogComponent;
+    @ViewChild('addrRequiredAlert')
+        private addrRequiredAlert: AlertDialogComponent;
 
 
     autoId = -1;
@@ -137,6 +139,8 @@ export class EditComponent implements OnInit, AfterViewInit {
     identTypes: ComboboxEntry[];
     inetLevels: ComboboxEntry[];
     statCats: StatCat[] = [];
+    grpList: IdlObject;
+    editProfiles: IdlObject[] = [];
     userStatCats: {[statId: number]: ComboboxEntry} = {};
     userSettings: {[name: string]: any} = {};
     userSettingTypes: {[name: string]: IdlObject} = {};
@@ -159,7 +163,7 @@ export class EditComponent implements OnInit, AfterViewInit {
     // All locations we have the specified permissions
     permOrgs: {[name: string]: number[]};
 
-    // True if a given perm is grnated at the current home_ou of the
+    // True if a given perm is granted at the current home_ou of the
     // patron we are editing.
     hasPerm: {[name: string]: boolean} = {};
 
@@ -187,6 +191,7 @@ export class EditComponent implements OnInit, AfterViewInit {
     }
 
     ngAfterViewInit() {
+        // Do this after view init so we can leverage
     }
 
     load(): Promise<any> {
@@ -198,6 +203,7 @@ export class EditComponent implements OnInit, AfterViewInit {
         .then(_ => this.getCloneUser())
         .then(_ => this.getSecondaryGroups())
         .then(_ => this.applyPerms())
+        .then(_ => this.setEditProfiles())
         .then(_ => this.setIdentTypes())
         .then(_ => this.setInetLevels())
         .then(_ => this.setOptInSettings())
@@ -206,6 +212,44 @@ export class EditComponent implements OnInit, AfterViewInit {
         .then(_ => this.loading = false);
     }
 
+    setEditProfiles(): Promise<any> {
+        return this.pcrud.retrieveAll('pgt', {}, {atomic: true}).toPromise()
+        .then(list => this.grpList = list)
+        .then(_ => this.applyEditProfiles());
+    }
+
+    // TODO
+    // Share the set of forbidden groups with the 2ndary groups selector.
+    applyEditProfiles(): Promise<any> {
+        const appPerms = [];
+        const failedPerms = [];
+        const profiles = this.grpList;
+
+        // extract the application permissions
+        profiles.forEach(grp => {
+            if (grp.application_perm()) {
+                appPerms.push(grp.application_perm());
+            }
+        });
+
+        const traverseTree = (grp: IdlObject, failed: boolean) => {
+            if (!grp) { return; }
+
+            failed = failed || failedPerms.includes(grp.application_perm());
+
+            if (!failed) { this.editProfiles.push(grp.id()); }
+
+            const children = profiles.filter(p => p.parent() === grp.id());
+            children.forEach(child => traverseTree(child, failed));
+        }
+
+        return this.perms.hasWorkPermAt(appPerms, true).then(orgs => {
+            appPerms.forEach(p => {
+                if (orgs[p].length === 0) { failedPerms.push(p); }
+                traverseTree(this.grpList[0], false);
+            });
+        });
+    }
 
     getCloneUser(): Promise<any> {
         if (!this.cloneId) { return Promise.resolve(); }
@@ -506,9 +550,9 @@ export class EditComponent implements OnInit, AfterViewInit {
     createNewPatron() {
         const patron = this.idl.create('au');
         patron.isnew(true);
-        patron.addresses([]);
+        patron.id(-1);
+        patron.home_ou(this.auth.user().ws_ou());
         patron.settings([]);
-        patron.cards([]);
         patron.waiver_entries([]);
         patron.stat_cat_entries([]);
 
@@ -516,7 +560,16 @@ export class EditComponent implements OnInit, AfterViewInit {
         card.isnew(true);
         card.usr(-1);
         patron.card(card);
-        patron.cards().push(card);
+        patron.cards([card]);
+
+        const addr = this.idl.create('aua');
+        addr.isnew(true);
+        addr.usr(-1);
+        addr.valid('t');
+        addr.country(this.context.settingsCache['ui.patron.default_country']);
+        patron.billing_address(addr);
+        patron.mailing_address(addr);
+        patron.addresses([addr]);
 
         this.patron = patron;
     }
@@ -557,14 +610,12 @@ export class EditComponent implements OnInit, AfterViewInit {
 
             const invalidInput = document.querySelector('.ng-invalid');
 
-            if (invalidInput) {
-                console.debug('Field is invalid', invalidInput.id);
-            }
-
             const canSave = (
-                invalidInput === null &&
-                !this.dupeBarcode &&
-                !this.dupeUsername
+                invalidInput === null
+                && !this.dupeBarcode
+                && !this.dupeUsername
+                && !this.selfEditForbidden()
+                && !this.groupEditForbidden()
             );
 
             this.toolbar.disableSaveStateChanged.emit(!canSave);
@@ -597,6 +648,15 @@ export class EditComponent implements OnInit, AfterViewInit {
 
     userSettingChange(name: string, value: any) {
         this.userSettings[name] = value;
+
+        switch (name) {
+            case 'opac.default_phone':
+            case 'opac.default_sms_notify':
+            case 'opac.default_sms_carrier':
+                // TODO hold related contact info updated
+                break;
+        }
+
         this.adjustSaveSate();
     }
 
@@ -648,22 +708,19 @@ export class EditComponent implements OnInit, AfterViewInit {
             `Modifying field path=${path || ''} field=${field} value=${value}`);
 
         switch (field) {
-            // TODO: do many more
+
+            case 'dob':
+                this.maintainJuvFlag();
+                break;
 
             case 'profile':
                 this.setExpireDate();
                 break;
 
             case 'day_phone':
-                // TODO: patron.password.use_phone
-                // TODO: hold related contact info
-                this.dupeValueChange(field, value);
-                break;
-
             case 'evening_phone':
             case 'other_phone':
-                // TODO hold related contact info
-                this.dupeValueChange(field, value);
+                this.handlePhoneChange(field, value);
                 break;
 
             case 'ident_value':
@@ -698,6 +755,39 @@ export class EditComponent implements OnInit, AfterViewInit {
         this.adjustSaveSate();
     }
 
+    maintainJuvFlag() {
+
+        if (!this.patron.dob()) { return; }
+
+        const interval =
+            this.context.settingsCache['global.juvenile_age_threshold']
+            || '18 years';
+
+        const cutoff = new Date();
+
+        cutoff.setTime(cutoff.getTime() -
+            Number(DateUtil.intervalToSeconds(interval) + '000'));
+
+        const isJuve = new Date(this.patron.dob()) > cutoff;
+
+        this.fieldValueChange(null, null, 'juvenile', isJuve);
+        this.afterFieldChange(null, null, 'juvenile');
+    }
+
+    handlePhoneChange(field: string, value: string) {
+        this.dupeValueChange(field, value);
+        // TODO: hold contact info stuff
+
+        const pwUsePhone =
+            this.context.settingsCache['patron.password.use_phone'];
+
+        if (field === 'day_phone' && value &&
+            this.patron.isnew() && !this.patron.passwd() && pwUsePhone) {
+            this.fieldValueChange(null, null, 'passwd', value.substr(-4));
+            this.afterFieldChange(null, null, 'passwd');
+        }
+    }
+
     handlePostCodeChange(addr: IdlObject, postCode: any) {
         this.net.request(
             'open-ils.search', 'open-ils.search.zip', postCode
@@ -750,6 +840,7 @@ export class EditComponent implements OnInit, AfterViewInit {
 
                 // Propagate username with barcode value by default.
                 // This will apply the value and fire the dupe checker
+                this.updateUsernameRegex();
                 this.fieldValueChange(null, null, 'usrname', value);
                 this.afterFieldChange(null, null, 'usrname');
             }
@@ -1025,8 +1116,9 @@ export class EditComponent implements OnInit, AfterViewInit {
         }
 
         promise.then(required => {
+
             if (required) {
-                // TODO alert and exit
+                this.addrRequiredAlert.open();
                 return;
             }
 
@@ -1173,10 +1265,6 @@ export class EditComponent implements OnInit, AfterViewInit {
     setFieldPatterns() {
         let regex;
 
-        if (regex = this.context.settingsCache['opac.username_regex']) {
-            this.fieldPatterns.au.usrname = new RegExp(regex);
-        }
-
         if (regex =
             this.context.settingsCache['ui.patron.edit.ac.barcode.regex']) {
             this.fieldPatterns.ac.barcode = new RegExp(regex);
@@ -1204,6 +1292,8 @@ export class EditComponent implements OnInit, AfterViewInit {
             const name = parts[2];
             this.fieldPatterns[cls][name] = new RegExp(val);
         });
+
+        this.updateUsernameRegex();
     }
 
     // The username must match either the configured regex or the
@@ -1224,6 +1314,20 @@ export class EditComponent implements OnInit, AfterViewInit {
             this.fieldPatterns.au.usrname = new RegExp('.*');
         }
     }
+
+    selfEditForbidden(): boolean {
+        return (
+            this.patron.id() === this.auth.user().id()
+            && !this.hasPerm.EDIT_SELF_IN_CLIENT
+        );
+    }
+
+    groupEditForbidden(): boolean {
+        return (
+            this.patron.profile()
+            && !this.editProfiles.includes(this.patron.profile())
+        );
+    }
 }
 
 
index 2ce1e1f..87fecbe 100644 (file)
@@ -16,13 +16,16 @@ const routes: Routes = [{
       import('./event-log/event-log.module').then(m => m.EventLogModule)
   }, {
     path: 'register',
-    component: RegisterPatronComponent
+    component: RegisterPatronComponent,
+    resolve: {resolver : PatronResolver}
   }, {
     path: 'register/clone/:cloneId',
-    component: RegisterPatronComponent
+    component: RegisterPatronComponent,
+    resolve: {resolver : PatronResolver}
   }, {
     path: 'register/stage/:stageUsername',
-    component: RegisterPatronComponent
+    component: RegisterPatronComponent,
+    resolve: {resolver : PatronResolver}
   }, {
     path: 'credentials',
     component: TestPatronPasswordComponent
index d6e777d..2a48ff8 100644 (file)
@@ -40,6 +40,8 @@ export class PatronStatCatsComponent implements OnInit {
             this.auth.token(), this.patronId, ['stat_cat_entries']).toPromise()
         .then(user => {
             const catIds = user.stat_cat_entries().map(e => e.stat_cat());
+            if (catIds.length === 0) { return; }
+
             this.pcrud.search('actsc', {id: catIds})
             .subscribe(cat => {
                 const map = user.stat_cat_entries()