+<eg-string key="circ.patron.edit.test_notify.success"
+ i18n-text text="Test Notification Sent"></eg-string>
+<eg-string key="circ.patron.edit.test_notify.fail"
+ i18n-text text="Test Notification Failed to Send"></eg-string>
+<eg-string key="circ.patron.edit.invalidate.success"
+ i18n-text text="Field Invalidation Succeeded"></eg-string>
+<eg-string key="circ.patron.edit.invalidate.fail"
+ i18n-text text="Failed to Invalidate Field"></eg-string>
+<eg-string key="circ.patron.edit.grplink.success"
+ i18n-text text="Group Link Succeeded"></eg-string>
+<eg-string key="circ.patron.edit.grplink.fail"
+ i18n-text text="Group Link Failed"></eg-string>
+
+<eg-patron-secondary-groups [secondaryGroups]="secondaryGroups" #secondaryGroupsDialog>
+</eg-patron-secondary-groups>
<div class="row" *ngIf="loading">
<div class="col-lg-6 offset-lg-3">
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"
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"
[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">
</eg-combobox>
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')">
</textarea>
<eg-date-select
domId="au-dob-input"
fieldName="au-dob-input"
+ [noMaxWidth]="true"
[initialIso]="patron.dob()"
(onChangeAsIso)="
fieldValueChange(null, 'dob', $event);
- postFieldChange(null, 'dob')"
+ afterFieldChange(null, 'dob')"
[required]="fieldRequired('au', 'dob')">
</eg-date-select>
</div>
<ng-container *ngTemplateOutlet="fieldRow; context:
{args: {template: fieldInput, field: 'ident_value2'}}">
</ng-container>
- <ng-container *ngTemplateOutlet="fieldRow; context:
- {args: {template: fieldInput, field: 'email', type: 'email'}}">
- </ng-container>
+
+
+ <div class="row pt-1 pb-1 mt-1">
+ <ng-container
+ *ngTemplateOutlet="fieldLabel; context: {args: {field: 'email'}}">
+ </ng-container>
+ <ng-container
+ *ngTemplateOutlet="fieldInput; context: {args: {field: 'email', type: 'email'}}">
+ </ng-container>
+ <div class="col-lg-6">
+ <button class="btn btn-outline-dark"
+ (click)="sendTestMessage('au.email.test')" i18n>
+ Send Test Email
+ </button>
+ <button class="btn btn-outline-dark ml-2"
+ (click)="invalidateField('email')" i18n>Invalidate</button>
+ </div>
+ </div>
+
<ng-container
*ngIf="userSettingTypes['circ.send_email_checkout_receipts'] && showField('au', 'email')">
<ng-container *ngTemplateOutlet="userSettingsCheckboxRow; context:
{args: {settingName: 'circ.send_email_checkout_receipts'}}">
</ng-container>
</ng-container>
- <ng-container *ngTemplateOutlet="fieldRow; context:
- {args: {template: fieldInput, field: 'day_phone'}}">
- </ng-container>
+
+ <div class="row pt-1 pb-1 mt-1">
+ <ng-container
+ *ngTemplateOutlet="fieldLabel; context: {args: {field: 'day_phone'}}">
+ </ng-container>
+ <ng-container
+ *ngTemplateOutlet="fieldInput; context: {args: {field: 'day_phone'}}">
+ </ng-container>
+ <div class="col-lg-6">
+ <button class="btn btn-outline-dark"
+ (click)="invalidateField('day_phone')" i18n>Invalidate</button>
+ </div>
+ </div>
+
<ng-container *ngTemplateOutlet="fieldRow; context:
{args: {template: fieldInput, field: 'evening_phone'}}">
</ng-container>
<ng-container *ngTemplateOutlet="fieldRow; context:
{args: {template: fieldInput, field: 'other_phone'}}">
</ng-container>
+
<div class="row pt-1 pb-1 mt-1" *ngIf="showField('au', 'home_ou')">
<ng-container
*ngTemplateOutlet="fieldLabel; context: {args: {field: 'home_ou'}}">
[disableOrgs]="cannotHaveUsersOrgs()"
(onChange)="
fieldValueChange(null, 'home_ou', $event ? $event.id() : null);
- postFieldChange(null, 'home_ou')">
+ afterFieldChange(null, 'home_ou')">
</eg-org-select>
</div>
</div>
+
<div class="row pt-1 pb-1 mt-1" *ngIf="showField('au', 'profile')">
<ng-container
*ngTemplateOutlet="fieldLabel; context: {args: {field: 'profile'}}">
[initialGroupId]="patron.profile()"
(profileChange)="
fieldValueChange(null, 'profile', $event ? $event.id() : null);
- postFieldChange(null, 'profile')">
+ afterFieldChange(null, 'profile')">
</eg-profile-select>
</div>
+ <div class="col-lg-6">
+ <button class="btn btn-outline-dark"
+ *ngIf="hasPerm.CREATE_USER_GROUP_LINK"
+ (click)="openGroupsDialog()" i18n>Secondary Groups</button>
+ </div>
</div>
+
<div class="row pt-1 pb-1 mt-1" *ngIf="showField('au', 'expire_date')">
<ng-container
*ngTemplateOutlet="fieldLabel; context: {args: {field: 'expire_date'}}">
<eg-date-select
domId="au-expire_date-input"
fieldName="au-expire_date-input"
+ [noMaxWidth]="true"
[required]="fieldRequired('au', 'expire_date')"
[(ngModel)]="expireDate">
</eg-date-select>
</div>
- <div class="col-lg-3">
+ <div class="col-lg-6">
<button class="btn btn-outline-dark" (click)="setExpireDate()" i18n>
Update Expire Date
</button>
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';
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',
'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: {
@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;
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,
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
) {}
load(): Promise<any> {
this.loading = true;
return this.loadPatron()
+ .then(_ => this.getSecondaryGroups())
+ .then(_ => this.applyPerms())
.then(_ => this.setIdentTypes())
.then(_ => this.setOptInSettings())
.finally(() => this.loading = false);
}
+ getSecondaryGroups(): Promise<any> {
+
+ 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<any> {
return this.patronService.getIdentTypes()
.then(types => {
});
}
+ applyPerms(): Promise<any> {
+
+ 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<any> {
const orgIds = this.org.ancestors(this.auth.user().ws_ou(), true);
createNewPatron() {
const patron = this.idl.create('au');
patron.isnew(true);
+ patron.addresses([]);
+ patron.settings([]);
const card = this.idl.create('ac');
card.isnew(true);
// 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;
}
// 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;
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');
}
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<boolean> {
+
+ 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<boolean> {
+
+ 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<boolean> {
+
+ 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<boolean> {
+
+ 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);
+ }
+ });
}
}
this.cellTextGenerator = {
barcode: row => row.card().barcode()
- }
+ };
this.dataSource.getRows = (pager: Pager, sort: any[]) =>
from(this.patrons.slice(pager.offset, pager.offset + pager.limit));
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(); } }
);
import {PatronStatCatsComponent} from './statcats.component';
import {PatronGroupComponent} from './group.component';
import {RegisterPatronComponent} from './register.component';
+import {SecondaryGroupsDialogComponent} from './secondary-groups.component';
@NgModule({
declarations: [
PatronSurveyResponsesComponent,
PatronGroupComponent,
RegisterPatronComponent,
- PatronStatCatsComponent
+ PatronStatCatsComponent,
+ SecondaryGroupsDialogComponent
],
imports: [
StaffCommonModule,
--- /dev/null
+<eg-string #successMsg text="Successfully Added Group" i18n-text></eg-string>
+<eg-string #errorMsg text="Failed To Add Group" i18n-text></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title" i18n>Secondary Permission Groups</h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <h5 i18n>
+ Assign additional permission groups to users here.
+ This does not affect circulation policy.
+ </h5>
+
+ <div class="row mt-2">
+ <div class="col-lg-6 font-weight-bold" i18n>Group</div>
+ <div class="col-lg-6 font-weight-bold" i18n>Action</div>
+ </div>
+
+ <div class="row mt-2" *ngFor="let grp of secondaryGroups">
+ <div class="col-lg-6">{{grp.name()}}</div>
+ <div class="col-lg-6">
+ <button class="btn btn-danger" *ngIf="!grp.isdeleted()"
+ (click)="grp.isdeleted(true)" i18n>Remove</button>
+ <button class="btn btn-info" *ngIf="grp.isdeleted()"
+ (click)="grp.isdeleted(false)" i18n>Un-Delete</button>
+ </div>
+ </div>
+
+ <div class="row mt-2" *ngFor="let grp of pendingGroups">
+ <div class="col-lg-6">{{grp.name()}}</div>
+ <div class="col-lg-6">
+ <button class="btn btn-warning"
+ (click)="removePending(grp)" i18n>Remove Pending</button>
+ </div>
+ </div>
+
+ <div class="row mt-2">
+ <div class="col-lg-6">
+ <eg-profile-select [(ngModel)]="selectedProfile"></eg-profile-select>
+ </div>
+ <div class="col-lg-6">
+ <button class="btn btn-success" (click)="add()" i18n>Add</button>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ (click)="applyChanges()" i18n>Apply Changes</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</button>
+ </div>
+</ng-template>
--- /dev/null
+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);
+ });
+ }
+}
+