mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
[HIMA-710] Implemented reminder popup that a remission process is started every 20 minutes or 40 inutes when user is idle
This commit is contained in:
@@ -43,6 +43,7 @@ import { GoodsInState } from './core/store/state/goods-in.state';
|
||||
import { BranchProcessState } from './core/store/state/branch-process.state';
|
||||
import { RemissionModule } from '@isa/remission';
|
||||
import { RemissionState } from './core/store/state/remission.state';
|
||||
import { NgIdleKeepaliveModule } from '@ng-idle/keepalive';
|
||||
|
||||
const states = [
|
||||
AppState,
|
||||
@@ -108,6 +109,7 @@ export function _feedServiceEndpointProviderFactory(conf: ConfigService) {
|
||||
animationDuration: 300,
|
||||
clockwise: true
|
||||
}),
|
||||
NgIdleKeepaliveModule.forRoot(),
|
||||
RemissionModule.forRoot({
|
||||
useMock: false,
|
||||
endpoints: {
|
||||
|
||||
@@ -18,4 +18,5 @@ export interface Remission {
|
||||
shippingDocument?: ShippingDocument;
|
||||
shippingDocumentCreated?: boolean;
|
||||
searchedProduct?: RemissionProduct;
|
||||
blockReminder?: boolean;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LoadCountries } from '../store/actions/countries.actions';
|
||||
import { AppState } from '../store/state/app.state';
|
||||
import { LoadBranches, LoadUserBranch } from '../store/actions/branch.actions';
|
||||
import { AppUserDataSync } from '../store/actions/app.actions';
|
||||
import { Subject, of } from 'rxjs';
|
||||
import { Subject, of, Observable, timer } from 'rxjs';
|
||||
import { takeUntil, catchError, take, filter, pairwise } from 'rxjs/operators';
|
||||
import { UserStateService } from './user-state.service';
|
||||
import { Meta, MetaDefinition } from '@angular/platform-browser';
|
||||
@@ -18,8 +18,11 @@ import { WindowRef } from './window-ref.service';
|
||||
import { LoadVats } from '../store/actions/vat.actions';
|
||||
import { LoadSuppliers } from '../store/actions/suppliers.actions';
|
||||
import { RemissionSelectors } from '../store/selectors/remission.selectors';
|
||||
import { Idle, DEFAULT_INTERRUPTSOURCES } from '@ng-idle/core';
|
||||
|
||||
declare let ga: Function;
|
||||
export const NOT_IDLE_REMISSION_REMINDER_MINUTES = 20;
|
||||
export const IDLE_REMISSION_REMINDER_MINUTES = 40;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -28,6 +31,8 @@ export class AppService implements OnDestroy {
|
||||
destroy$ = new Subject();
|
||||
loader$ = new Subject();
|
||||
levingRemission$ = new Subject();
|
||||
remissionReminder$ = new Subject();
|
||||
timerClear$ = new Subject();
|
||||
constructor(
|
||||
private ssoService: SsoService,
|
||||
private store: Store,
|
||||
@@ -36,7 +41,8 @@ export class AppService implements OnDestroy {
|
||||
private route: ActivatedRoute,
|
||||
private http: HttpClient,
|
||||
public router: Router,
|
||||
private windowRef: WindowRef
|
||||
private windowRef: WindowRef,
|
||||
private idle: Idle
|
||||
) {}
|
||||
|
||||
public appLoadInitialisations() {
|
||||
@@ -63,6 +69,8 @@ export class AppService implements OnDestroy {
|
||||
this.loadCountries();
|
||||
this.loadVats();
|
||||
this.loadSuppliers();
|
||||
this.shouldRemindForRemissionAtAppStart();
|
||||
this.registerIdleManager();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -145,11 +153,76 @@ export class AppService implements OnDestroy {
|
||||
const isRemissionCompleated = this.store.selectSnapshot(RemissionSelectors.getRemissionCompleatedStatus);
|
||||
isRemissionCompleated.ifFalse(() => {
|
||||
this.levingRemission$.next();
|
||||
this.registerTimedRemissionReminder(NOT_IDLE_REMISSION_REMINDER_MINUTES);
|
||||
});
|
||||
});
|
||||
(isCommingFromRemission && isGoingAwayFromRemission).ifFalse(() => {
|
||||
this.timerClear$.next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRemindForRemissionAtAppStart() {
|
||||
this.store
|
||||
.select(RemissionSelectors.shouldRemindForRemission)
|
||||
.pipe(
|
||||
filter(data => !isNullOrUndefined(data)),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(status => {
|
||||
const isNotAtRemission = !this.router.url.includes('remission');
|
||||
(status && isNotAtRemission).ifTrue(() => {
|
||||
this.remissionReminder$.next();
|
||||
this.registerTimedRemissionReminder(NOT_IDLE_REMISSION_REMINDER_MINUTES);
|
||||
});
|
||||
isNotAtRemission.ifFalse(() => {
|
||||
this.timerClear$.next();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private registerTimedRemissionReminder(minutes: number) {
|
||||
const emittingTime = 1000 * 60 * minutes;
|
||||
const timer$: Observable<number> = timer(0, emittingTime);
|
||||
timer$.pipe(takeUntil(this.timerClear$)).subscribe(interval => {
|
||||
(interval > 0).ifTrue(() => {
|
||||
this.remissionReminder$.next();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private registerIdleManager() {
|
||||
this.idle.setIdle(60);
|
||||
this.idle.setInterrupts(DEFAULT_INTERRUPTSOURCES);
|
||||
this.idle.onIdleStart.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
this.timerClear$.next();
|
||||
this.registerTimedRemissionReminder(IDLE_REMISSION_REMINDER_MINUTES);
|
||||
});
|
||||
this.idle.onIdleEnd.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
this.timerClear$.next();
|
||||
this.returToActiveStateReminder();
|
||||
});
|
||||
this.idle.watch();
|
||||
}
|
||||
|
||||
private returToActiveStateReminder() {
|
||||
this.store
|
||||
.select(RemissionSelectors.shouldRemindForRemission)
|
||||
.pipe(
|
||||
filter(data => !isNullOrUndefined(data)),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(status => {
|
||||
const isNotAtRemission = !this.router.url.includes('remission');
|
||||
(status && isNotAtRemission).ifTrue(() => {
|
||||
this.registerTimedRemissionReminder(NOT_IDLE_REMISSION_REMINDER_MINUTES);
|
||||
});
|
||||
isNotAtRemission.ifFalse(() => {
|
||||
this.timerClear$.next();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private sendNavigationEventsToGoogleAnalytics(url: string) {
|
||||
ga('set', 'page', url);
|
||||
ga('send', 'pageview');
|
||||
@@ -157,6 +230,7 @@ export class AppService implements OnDestroy {
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.timerClear$.next();
|
||||
}
|
||||
|
||||
private testHttpCall() {
|
||||
|
||||
@@ -17,6 +17,7 @@ export const SET_REMISSION_TARGET = '[REMISSION] Set target';
|
||||
export const SET_REMISSION_PRODUCTS = '[REMISSION] Set products';
|
||||
export const SET_SHIPPING_DOCUMENT = '[REMISSION] Set shipping document';
|
||||
export const SET_REMISSION_SEARCHED_PRODUCT = '[REMISSION] Set remission searched product';
|
||||
export const ACTIVATE_REMISSION_REMINDER = '[REMISSION] activate reminder';
|
||||
|
||||
export class SetRemissionCreated {
|
||||
static readonly type = SET_REMISSION_CREATED;
|
||||
@@ -95,3 +96,7 @@ export class SetRemissionSearchedProduct {
|
||||
|
||||
constructor(public product?: RemissionProduct) {}
|
||||
}
|
||||
|
||||
export class ActivateRemissionReminder {
|
||||
static readonly type = ACTIVATE_REMISSION_REMINDER;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ export class RemissionSelectors {
|
||||
return remission.remissionProcessCompleted;
|
||||
}
|
||||
|
||||
@Selector([RemissionState])
|
||||
static shouldRemindForRemission(remissionState: RemissionStateModel) {
|
||||
const remission = remissionState.remission;
|
||||
return remission.remissionProcessCreated && !remission.remissionProcessCompleted && !remission.blockReminder;
|
||||
}
|
||||
|
||||
@Selector([RemissionState])
|
||||
static getRemissionShippingDocumentstatus(remissionState: RemissionStateModel) {
|
||||
const remission = remissionState.remission;
|
||||
|
||||
@@ -22,7 +22,7 @@ import { ReloadGoodsIn } from '../actions/goods-in.actions';
|
||||
import { ReloadBranchProcess } from '../actions/branch-process.actions';
|
||||
import { ReloadRemission } from '../actions/remission.actions';
|
||||
|
||||
export const SYNC_DATA_VERSION = 110;
|
||||
export const SYNC_DATA_VERSION = 117;
|
||||
|
||||
export class AppStateModel {
|
||||
currentProcesssId: number;
|
||||
|
||||
@@ -122,6 +122,15 @@ export class RemissionState {
|
||||
this.syncApiState(remission);
|
||||
}
|
||||
|
||||
@Action(actions.ActivateRemissionReminder)
|
||||
activateRemissionReminder(ctx: StateContext<RemissionStateModel>) {
|
||||
const state = ctx.getState();
|
||||
const currentRemission = state.remission;
|
||||
const remission: Remission = { ...currentRemission, blockReminder: false };
|
||||
ctx.patchState({ remission });
|
||||
this.syncApiState(remission);
|
||||
}
|
||||
|
||||
@Action(actions.SetRemissionShippingDocument)
|
||||
setRemissionShippingDocument(ctx: StateContext<RemissionStateModel>, { shippingDocument }: actions.SetRemissionShippingDocument) {
|
||||
const state = ctx.getState();
|
||||
@@ -176,7 +185,8 @@ export class RemissionState {
|
||||
centralTarget: Side.LEFT,
|
||||
overflowTarget: Side.LEFT,
|
||||
source: RemissionResourceType.Central,
|
||||
shippingDocumentCreated: false
|
||||
shippingDocumentCreated: false,
|
||||
blockReminder: true
|
||||
};
|
||||
ctx.patchState({ remission });
|
||||
this.syncApiState(remission);
|
||||
@@ -192,7 +202,8 @@ export class RemissionState {
|
||||
centralTarget: Side.LEFT,
|
||||
overflowTarget: Side.LEFT,
|
||||
source: RemissionResourceType.Central,
|
||||
shippingDocumentCreated: false
|
||||
shippingDocumentCreated: false,
|
||||
blockReminder: true
|
||||
};
|
||||
ctx.patchState({ remission });
|
||||
this.syncApiState(remission);
|
||||
@@ -207,7 +218,8 @@ export class RemissionState {
|
||||
centralTarget: Side.LEFT,
|
||||
overflowTarget: Side.LEFT,
|
||||
source: RemissionResourceType.Central,
|
||||
shippingDocumentCreated: false
|
||||
shippingDocumentCreated: false,
|
||||
blockReminder: true
|
||||
};
|
||||
ctx.patchState({ remission });
|
||||
this.syncApiState(remission);
|
||||
|
||||
@@ -29,6 +29,7 @@ import { LogOutComponent } from '../components/log-out/log-out.component';
|
||||
import { ErrorModule } from '../core/error/component/error.module';
|
||||
import { DoubleChoiceSwitchModule } from '@libs/ui/lib/double-choice-switch';
|
||||
import { RemissionLeaveDialogComponent } from './remission/components/remission-leave-dialog/remission-leave-dialog.component';
|
||||
import { RemissionReminderDialogComponent } from './remission/components/remission-reminder-dialog/remission-reminder-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -39,7 +40,8 @@ import { RemissionLeaveDialogComponent } from './remission/components/remission-
|
||||
MenuComponent,
|
||||
BreadcrumbsComponent,
|
||||
LogOutComponent,
|
||||
RemissionLeaveDialogComponent
|
||||
RemissionLeaveDialogComponent,
|
||||
RemissionReminderDialogComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -73,7 +75,8 @@ import { RemissionLeaveDialogComponent } from './remission/components/remission-
|
||||
InfiniteScrollModule,
|
||||
ProcessModule,
|
||||
LogOutComponent,
|
||||
RemissionLeaveDialogComponent
|
||||
RemissionLeaveDialogComponent,
|
||||
RemissionReminderDialogComponent
|
||||
]
|
||||
})
|
||||
export class ComponentsModule {}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<app-modal id="remission-reminder-modal" branch="true">
|
||||
<div class="modal-wrapper">
|
||||
<div class="header">
|
||||
<lib-icon (click)="closeDialog()" height="21px" class="close-icon" name="close-branch" alt="close"></lib-icon>
|
||||
</div>
|
||||
<div class="title">
|
||||
<span>Offene Remi Liste</span>
|
||||
</div>
|
||||
<div class="sub-title">
|
||||
<span class="align-center">Sie haben eine offene Remi Liste. Kehren Sie zurück zur Remission, um die Remi Liste abzuschließen.</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<app-button (click)="toRemission()" primaryBorders="true" primary="true">Zur Remission</app-button>
|
||||
</div>
|
||||
</div>
|
||||
</app-modal>
|
||||
@@ -0,0 +1,57 @@
|
||||
.modal-wrapper {
|
||||
font-family: 'Open Sans';
|
||||
line-height: 21px;
|
||||
margin: 16px 0 40px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 210px;
|
||||
|
||||
.header {
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
right: 25px;
|
||||
}
|
||||
|
||||
.close-icon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-top: 36px;
|
||||
span {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-top: 26px;
|
||||
span {
|
||||
font-family: 'Open Sans';
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
max-width: 546px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-top: 26px;
|
||||
|
||||
.close-button {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RemissionReminderDialogComponent } from './remission-reminder-dialog.component';
|
||||
|
||||
describe('RemissionReminderDialogComponent', () => {
|
||||
let component: RemissionReminderDialogComponent;
|
||||
let fixture: ComponentFixture<RemissionReminderDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ RemissionReminderDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RemissionReminderDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ModalService } from '@libs/ui';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngxs/store';
|
||||
import { ModuleSwitcherService } from 'apps/sales/src/app/core/services/module-switcher.service';
|
||||
import { ResetBreadcrumbsTo } from 'apps/sales/src/app/core/store/actions/breadcrumb.actions';
|
||||
import { SetBranchProcessCurrentPath } from 'apps/sales/src/app/core/store/actions/branch-process.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'app-remission-reminder-dialog',
|
||||
templateUrl: './remission-reminder-dialog.component.html',
|
||||
styleUrls: ['./remission-reminder-dialog.component.scss']
|
||||
})
|
||||
export class RemissionReminderDialogComponent implements OnInit {
|
||||
id = 'remission-reminder-modal';
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private router: Router,
|
||||
private store: Store,
|
||||
private moduleSwitcher: ModuleSwitcherService
|
||||
) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
public openDialog() {
|
||||
this.modalService.open(this.id);
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.modalService.close(this.id);
|
||||
}
|
||||
|
||||
toRemission() {
|
||||
this.closeDialog();
|
||||
this.moduleSwitcher.switchToBranch();
|
||||
this.navigateRemissionCreateList();
|
||||
}
|
||||
|
||||
navigateRemissionCreateList() {
|
||||
const path = '/remission/create';
|
||||
this.store.dispatch(
|
||||
new ResetBreadcrumbsTo(
|
||||
{
|
||||
path: path,
|
||||
name: 'Remission'
|
||||
},
|
||||
'remission'
|
||||
)
|
||||
);
|
||||
this.router.navigate([path]);
|
||||
this.store.dispatch(new SetBranchProcessCurrentPath(path, true));
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
SetRemissionSource,
|
||||
SetRemissionTarget,
|
||||
SetRemissionStarted,
|
||||
SetRemissionSearchedProduct
|
||||
SetRemissionSearchedProduct,
|
||||
ActivateRemissionReminder
|
||||
} from 'apps/sales/src/app/core/store/actions/remission.actions';
|
||||
import { RemissionHelperService } from '../../services/remission-helper.service';
|
||||
import { UpdateFilter } from '../../models/update-filter.model';
|
||||
@@ -261,7 +262,12 @@ export class RemissionListCreateComponent implements OnInit, OnDestroy {
|
||||
(this.selectedRemissionResourceType === RemissionResourceType.Overflow).ifTrue(() =>
|
||||
this.shouldShowOverflowInitialMessage(remissionProcess.filter)
|
||||
);
|
||||
this.store.dispatch(new SetRemissionProcess(remissionProcess));
|
||||
this.store
|
||||
.dispatch(new SetRemissionProcess(remissionProcess))
|
||||
.toPromise()
|
||||
.then(() => {
|
||||
this.store.dispatch(new ActivateRemissionReminder());
|
||||
});
|
||||
};
|
||||
|
||||
shouldShowOverflowInitialMessage(remissionFilter: RemissionFilter) {
|
||||
|
||||
@@ -8,3 +8,4 @@
|
||||
</div>
|
||||
<app-error></app-error>
|
||||
<app-remission-leave-dialog></app-remission-leave-dialog>
|
||||
<app-remission-reminder-dialog></app-remission-reminder-dialog>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { ModuleSwitcher } from '../../core/models/app-switcher.enum';
|
||||
import { ModuleSwitcherService } from '../../core/services/module-switcher.service';
|
||||
import { RemissionLeaveDialogComponent } from '../../modules/remission/components/remission-leave-dialog/remission-leave-dialog.component';
|
||||
import { AppService } from '../../core/services/app.service';
|
||||
// tslint:disable-next-line: max-line-length
|
||||
import { RemissionReminderDialogComponent } from '../../modules/remission/components/remission-reminder-dialog/remission-reminder-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-content',
|
||||
@@ -17,6 +19,7 @@ import { AppService } from '../../core/services/app.service';
|
||||
export class ContentPageComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(ErrorComponent) element: ErrorComponent;
|
||||
@ViewChild(RemissionLeaveDialogComponent) remissionLeavingDialog: RemissionLeaveDialogComponent;
|
||||
@ViewChild(RemissionReminderDialogComponent) remissionReminderDialog: RemissionReminderDialogComponent;
|
||||
module: ModuleSwitcher = ModuleSwitcher.Customer;
|
||||
destroy$ = new Subject();
|
||||
constructor(
|
||||
@@ -35,6 +38,10 @@ export class ContentPageComponent implements OnInit, OnDestroy {
|
||||
this.appService.levingRemission$.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
this.remissionLeavingDialog.openDialog();
|
||||
});
|
||||
|
||||
this.appService.remissionReminder$.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
this.remissionReminderDialog.openDialog();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1102,6 +1102,22 @@
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"@ng-idle/core": {
|
||||
"version": "8.0.0-beta.4",
|
||||
"resolved": "https://registry.npmjs.org/@ng-idle/core/-/core-8.0.0-beta.4.tgz",
|
||||
"integrity": "sha512-bI/tt7F5+z/w6b6SzNPc0+9rJ1knWgLzQweLd6xUQ7HHFrHsmBlJYUGiSv0cYUY+2RZAmIDbevwUDKLWmdE55w==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"@ng-idle/keepalive": {
|
||||
"version": "8.0.0-beta.4",
|
||||
"resolved": "https://registry.npmjs.org/@ng-idle/keepalive/-/keepalive-8.0.0-beta.4.tgz",
|
||||
"integrity": "sha512-yj7Y0pu2C7LhcjTcBE9DK5mFPFVHDj3vIVPTDzK3dN+51WoDQMrELl7BMuvzmTViat7mjsSP+00EwFvg6+FDEQ==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"@ngtools/json-schema": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngtools/json-schema/-/json-schema-1.1.0.tgz",
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
"@isa/print-api": "^0.0.25",
|
||||
"@isa/remi-api": "^0.0.25",
|
||||
"@isa/remission": "^0.1.15",
|
||||
"@ng-idle/core": "^8.0.0-beta.4",
|
||||
"@ng-idle/keepalive": "^8.0.0-beta.4",
|
||||
"@ngxs/store": "^3.4.1",
|
||||
"@types/faker": "^4.1.5",
|
||||
"@zxing/ngx-scanner": "^1.3.0",
|
||||
|
||||
Reference in New Issue
Block a user