From 0a3d78dea24bffac5103f9f78628f4eadba08048 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Fri, 19 Mar 2021 12:41:11 -0400 Subject: [PATCH] LP1904036 Patron editor field invalidate; secondary grps Signed-off-by: Bill Erickson Signed-off-by: Jane Sandberg Signed-off-by: Galen Charlton --- .../src/app/staff/circ/patron/edit.component.html | 80 +++++++-- .../src/app/staff/circ/patron/edit.component.ts | 183 ++++++++++++++++++++- .../src/app/staff/circ/patron/group.component.ts | 4 +- .../eg2/src/app/staff/circ/patron/patron.module.ts | 4 +- .../circ/patron/secondary-groups.component.html | 56 +++++++ .../circ/patron/secondary-groups.component.ts | 78 +++++++++ 6 files changed, 380 insertions(+), 25 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.ts diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.html index 6481774ff0..8e562b7277 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.html +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.html @@ -1,3 +1,18 @@ + + + + + + + + +
@@ -25,7 +40,7 @@ id="{{getClass(args.cls)}}-{{args.field}}-input" [ngModel]="objectFromPath(args.path)[args.field]()" (ngModelChange)="fieldValueChange(args.path, args.field, $event)" - (change)="postFieldChange(args.path, args.field)" + (change)="afterFieldChange(args.path, args.field)" [required]="fieldRequired(getClass(args.cls), args.field)" [pattern]="fieldPattern(getClass(args.cls), args.field)" [disabled]="args.disabled" @@ -43,7 +58,7 @@ id="{{getClass(args.cls)}}-{{args.field}}-input" [ngModel]="objectFromPath(args.path)[args.field]() == 't'" (ngModelChange)="fieldValueChange(args.path, args.field, $event)" - (change)="postFieldChange(args.path, args.field)" + (change)="afterFieldChange(args.path, args.field)" [required]="fieldRequired(getClass(args.cls), args.field)" [pattern]="fieldPattern(getClass(args.cls), args.field)" [disabled]="disabled" @@ -60,7 +75,7 @@ [startId]="getFieldValue(args.path, args.field)" (onChange)=" fieldValueChange(args.path, args.field, $event ? $event.id : null); - postFieldChange(args.path, args.field)" + afterFieldChange(args.path, args.field)" [required]="fieldRequired(getClass(args.cls), args.field)" [disabled]="args.disabled"> @@ -187,7 +202,7 @@ id="au-name_keywords-input" [ngModel]="objectFromPath(null)['name_keywords']()" (ngModelChange)="fieldValueChange(null, 'name_keywords', $event)" - (change)="postFieldChange(null, 'name_keywords')" + (change)="afterFieldChange(null, 'name_keywords')" [required]="fieldRequired('au', 'name_keywords')" [pattern]="fieldPattern('au', 'name_keywords')"> @@ -208,10 +223,11 @@
@@ -234,24 +250,52 @@ - - + + +
+ + + + +
+ + +
+
+ - - + +
+ + + + +
+ +
+
+ +
@@ -264,10 +308,11 @@ [disableOrgs]="cannotHaveUsersOrgs()" (onChange)=" fieldValueChange(null, 'home_ou', $event ? $event.id() : null); - postFieldChange(null, 'home_ou')"> + afterFieldChange(null, 'home_ou')">
+
@@ -278,10 +323,16 @@ [initialGroupId]="patron.profile()" (profileChange)=" fieldValueChange(null, 'profile', $event ? $event.id() : null); - postFieldChange(null, 'profile')"> + afterFieldChange(null, 'profile')">
+
+ +
+
@@ -290,11 +341,12 @@
-
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.ts index 884ed01019..c7589248c3 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/edit.component.ts @@ -1,5 +1,6 @@ import {Component, OnInit, Input, ViewChild} from '@angular/core'; import {Router, ActivatedRoute, ParamMap} from '@angular/router'; +import {concatMap, tap} from 'rxjs/operators'; import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap'; import {OrgService} from '@eg/core/org.service'; import {IdlService, IdlObject} from '@eg/core/idl.service'; @@ -11,6 +12,11 @@ import {PatronContextService} from './patron.service'; import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component'; import {DateUtil} from '@eg/share/util/date'; import {ProfileSelectComponent} from '@eg/staff/share/patron/profile-select.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {StringService} from '@eg/share/string/string.service'; +import {EventService} from '@eg/core/event.service'; +import {PermService} from '@eg/core/perm.service'; +import {SecondaryGroupsDialogComponent} from './secondary-groups.component'; const COMMON_USER_SETTING_TYPES = [ 'circ.holds_behind_desk', @@ -22,6 +28,18 @@ const COMMON_USER_SETTING_TYPES = [ 'opac.default_sms_notify' ]; +const PERMS_NEEDED = [ + 'EDIT_SELF_IN_CLIENT', + 'UPDATE_USER', + 'CREATE_USER', + 'CREATE_USER_GROUP_LINK', + 'UPDATE_PATRON_COLLECTIONS_EXEMPT', + 'UPDATE_PATRON_CLAIM_RETURN_COUNT', + 'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT', + 'UPDATE_PATRON_ACTIVE_CARD', + 'UPDATE_PATRON_PRIMARY_CARD' +]; + const FLESH_PATRON_FIELDS = { flesh: 1, flesh_fields: { @@ -40,7 +58,10 @@ export class EditComponent implements OnInit { @Input() cloneId: number; @Input() stageUsername: string; - @ViewChild('profileSelect') private profileSelect: ProfileSelectComponent; + @ViewChild('profileSelect') + private profileSelect: ProfileSelectComponent; + @ViewChild('secondaryGroupsDialog') + private secondaryGroupsDialog: SecondaryGroupsDialogComponent; patron: IdlObject; changeHandlerNeeded = false; @@ -48,11 +69,18 @@ export class EditComponent implements OnInit { loading = false; identTypes: ComboboxEntry[]; - profileGroups: ComboboxEntry[]; userSettings: {[name: string]: any} = {}; userSettingTypes: {[name: string]: IdlObject} = {}; optInSettingTypes: {[name: string]: IdlObject} = {}; expireDate: Date; + secondaryGroups: IdlObject[]; + + // 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 + // patron we are editing. + hasPerm: {[name: string]: boolean} = {}; constructor( private org: OrgService, @@ -60,7 +88,11 @@ export class EditComponent implements OnInit { private auth: AuthService, private pcrud: PcrudService, private idl: IdlService, - public patronService: PatronService, + private strings: StringService, + private toast: ToastService, + private perms: PermService, + private evt: EventService, + private patronService: PatronService, public context: PatronContextService ) {} @@ -71,11 +103,26 @@ export class EditComponent implements OnInit { load(): Promise { this.loading = true; return this.loadPatron() + .then(_ => this.getSecondaryGroups()) + .then(_ => this.applyPerms()) .then(_ => this.setIdentTypes()) .then(_ => this.setOptInSettings()) .finally(() => this.loading = false); } + getSecondaryGroups(): Promise { + + return this.net.request( + 'open-ils.actor', + 'open-ils.actor.user.get_groups', + this.auth.token(), this.patronId + + ).pipe(concatMap(maps => this.pcrud.search('pgt', + {id: maps.map(m => m.grp())}, {}, {atomic: true}) + + )).pipe(tap(grps => this.secondaryGroups = grps)).toPromise(); + } + setIdentTypes(): Promise { return this.patronService.getIdentTypes() .then(types => { @@ -83,6 +130,21 @@ export class EditComponent implements OnInit { }); } + applyPerms(): Promise { + + const promise = this.permOrgs ? + Promise.resolve(this.permOrgs) : + this.perms.hasWorkPermAt(PERMS_NEEDED, true); + + return promise.then(permOrgs => { + this.permOrgs = permOrgs; + Object.keys(permOrgs).forEach(perm => + this.hasPerm[perm] = + permOrgs[perm].includes(this.patron.home_ou()) + ); + }); + } + setOptInSettings(): Promise { const orgIds = this.org.ancestors(this.auth.user().ws_ou(), true); @@ -140,6 +202,8 @@ export class EditComponent implements OnInit { createNewPatron() { const patron = this.idl.create('au'); patron.isnew(true); + patron.addresses([]); + patron.settings([]); const card = this.idl.create('ac'); card.isnew(true); @@ -175,7 +239,7 @@ export class EditComponent implements OnInit { // Called as the model changes. // This may be called many times before the final value is applied, - // so avoid any heavy lifting here. See postFieldChange(); + // so avoid any heavy lifting here. See afterFieldChange(); fieldValueChange(path: string, field: string, value: any) { if (typeof value === 'boolean') { value = value ? 't' : 'f'; } this.changeHandlerNeeded = true; @@ -183,7 +247,7 @@ export class EditComponent implements OnInit { } // Called after a change operation has completed (e.g. on blur) - postFieldChange(path: string, field: string) { + afterFieldChange(path: string, field: string) { if (!this.changeHandlerNeeded) { return; } // no changes applied this.changeHandlerNeeded = false; @@ -222,11 +286,11 @@ export class EditComponent implements OnInit { generatePassword() { this.fieldValueChange(null, - 'passwd', Math.floor(Math.random()*9000) + 1000); + 'passwd', Math.floor(Math.random() * 9000) + 1000); // Normally this is called on (blur), but the input is not // focused when using the generate button. - this.postFieldChange(null, 'passwd'); + this.afterFieldChange(null, 'passwd'); } @@ -245,7 +309,110 @@ export class EditComponent implements OnInit { const newDate = new Date(nowEpoch + (seconds * 1000 /* millis */)); this.expireDate = newDate; this.fieldValueChange(null, 'profile', newDate.toISOString()); - this.postFieldChange(null, 'profile'); + this.afterFieldChange(null, 'profile'); + } + + handleBoolResponse(success: boolean, + msg: string, errMsg?: string): Promise { + + if (success) { + return this.strings.interpolate(msg) + .then(str => this.toast.success(str)) + .then(_ => true); + } + + console.error(errMsg); + + return this.strings.interpolate(msg) + .then(str => this.toast.danger(str)) + .then(_ => false); + } + + sendTestMessage(hook: string): Promise { + + return this.net.request( + 'open-ils.actor', + 'open-ils.actor.event.test_notification', + this.auth.token(), {hook: hook, target: this.patronId} + ).toPromise().then(resp => { + + if (resp && resp.template_output && resp.template_output() && + resp.template_output().is_error() === 'f') { + return this.handleBoolResponse( + true, 'circ.patron.edit.test_notify.success'); + + } else { + return this.handleBoolResponse( + false, 'circ.patron.edit.test_notify.fail', + 'Test Notification Failed ' + resp); + } + }); + } + + invalidateField(field: string): Promise { + + return this.net.request( + 'open-ils.actor', + 'open-ils.actor.invalidate.' + field, + this.auth.token(), this.patronId, null, this.patron.home_ou() + + ).toPromise().then(resp => { + const evt = this.evt.parse(resp); + + if (evt && evt.textcode !== 'SUCCESS') { + return this.handleBoolResponse(false, + 'circ.patron.edit.invalidate.fail', + 'Field Invalidation Failed: ' + resp); + } + + this.patron[field](null); + + // Keep this in sync for future updates. + this.patron.last_xact_id(resp.payload.last_xact_id[this.patronId]); + + return this.handleBoolResponse( + true, 'circ.patron.edit.invalidate.success'); + }); + } + + openGroupsDialog() { + this.secondaryGroupsDialog.open({size: 'lg'}).subscribe(groups => { + if (!groups) { return; } + + this.secondaryGroups = groups; + + if (this.patron.isnew()) { + // Links will be applied after the patron is created. + return; + } + + // Apply the new links to an existing user in real time + this.applySecondaryGroups(); + }); + } + + applySecondaryGroups(): Promise { + + const groupIds = this.secondaryGroups.map(grp => grp.id()); + + return this.net.request( + 'open-ils.actor', + 'open-ils.actor.user.set_groups', + this.auth.token(), this.patronId, groupIds + ).toPromise().then(resp => { + + if (Number(resp) === 1) { + + return this.handleBoolResponse( + true, 'circ.patron.edit.grplink.success'); + + } else { + + return this.handleBoolResponse( + false, 'circ.patron.edit.grplink.fail', + 'Failed to change group links: ' + resp); + } + }); } } diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/group.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/group.component.ts index 1cbd00adaa..1221995c99 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/group.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/group.component.ts @@ -54,7 +54,7 @@ export class PatronGroupComponent implements OnInit { this.cellTextGenerator = { barcode: row => row.card().barcode() - } + }; this.dataSource.getRows = (pager: Pager, sort: any[]) => from(this.patrons.slice(pager.offset, pager.offset + pager.limit)); @@ -127,7 +127,7 @@ export class PatronGroupComponent implements OnInit { this.auth.token(), user ); })).subscribe( - resp => { if (this.evt.parse(resp)) { allOk = false; } }, + resp2 => { if (this.evt.parse(resp2)) { allOk = false; } }, err => console.error(err), () => { if (allOk) { this.refresh(); } } ); diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts index dc31125ca8..e4bed3ba32 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts @@ -27,6 +27,7 @@ import {PatronSurveyResponsesComponent} from './surveys.component'; import {PatronStatCatsComponent} from './statcats.component'; import {PatronGroupComponent} from './group.component'; import {RegisterPatronComponent} from './register.component'; +import {SecondaryGroupsDialogComponent} from './secondary-groups.component'; @NgModule({ declarations: [ @@ -45,7 +46,8 @@ import {RegisterPatronComponent} from './register.component'; PatronSurveyResponsesComponent, PatronGroupComponent, RegisterPatronComponent, - PatronStatCatsComponent + PatronStatCatsComponent, + SecondaryGroupsDialogComponent ], imports: [ StaffCommonModule, diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.html new file mode 100644 index 0000000000..1f2e7def17 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.html @@ -0,0 +1,56 @@ + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.ts new file mode 100644 index 0000000000..bac6b1af36 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/secondary-groups.component.ts @@ -0,0 +1,78 @@ +import {Component, OnInit, Input, ViewChild} from '@angular/core'; +import {Observable, empty} from 'rxjs'; +import {switchMap, tap} from 'rxjs/operators'; +import {IdlObject, IdlService} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {EventService} from '@eg/core/event.service'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {AuthService} from '@eg/core/auth.service'; +import {OrgService} from '@eg/core/org.service'; +import {DialogComponent} from '@eg/share/dialog/dialog.component'; +import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap'; +import {StringComponent} from '@eg/share/string/string.component'; + +/* Add/Remove Secondary Groups */ + +@Component({ + selector: 'eg-patron-secondary-groups', + templateUrl: 'secondary-groups.component.html' +}) + +export class SecondaryGroupsDialogComponent + extends DialogComponent implements OnInit { + + @Input() secondaryGroups: IdlObject[] = []; // pgt + selectedProfile: IdlObject; + pendingGroups: IdlObject[]; + + constructor( + private modal: NgbModal, + private toast: ToastService, + private net: NetService, + private idl: IdlService, + private evt: EventService, + private pcrud: PcrudService, + private org: OrgService, + private auth: AuthService) { + super(modal); + } + + ngOnInit() { + this.onOpen$.subscribe(_ => { + this.pendingGroups = []; + this.selectedProfile = null; + }); + } + + add() { + if (this.selectedProfile) { + this.pendingGroups.push(this.selectedProfile); + this.selectedProfile = null; + } + } + + removePending(grp: IdlObject) { + this.pendingGroups = + this.pendingGroups.filter(p => p.id() !== grp.id()); + } + + remove(grp: IdlObject) { + grp.deleted(true); + } + + applyChanges() { + this.close( + this.pendingGroups.concat( + this.secondaryGroups.filter(g => !g.isdeleted())) + ); + + // Reset the flags on the group objects so there's no + // unintended side effects. + this.secondaryGroups.concat(this.pendingGroups).forEach(grp => { + grp.isnew(null); + grp.isdeleted(null); + }); + } +} + -- 2.11.0