Merge pull request #4183 from MaximilianKrambach/presenceCheck
Set/unset presence
This commit is contained in:
commit
c406362da5
@ -15,8 +15,11 @@
|
|||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<mat-drawer-container class="on-transition-fade">
|
<mat-drawer-container class="on-transition-fade">
|
||||||
<os-sort-filter-bar [sortService]="sortService" [filterService]="filterService"
|
<os-sort-filter-bar
|
||||||
(searchFieldChange)="searchFilter($event)">
|
[sortService]="sortService"
|
||||||
|
[filterService]="filterService"
|
||||||
|
(searchFieldChange)="searchFilter($event)"
|
||||||
|
>
|
||||||
</os-sort-filter-bar>
|
</os-sort-filter-bar>
|
||||||
|
|
||||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||||
@ -46,7 +49,7 @@
|
|||||||
<ng-container matColumnDef="group">
|
<ng-container matColumnDef="group">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let user">
|
<mat-cell *matCellDef="let user">
|
||||||
<div class='groupsCell'>
|
<div class="groupsCell">
|
||||||
<span *ngIf="user.groups && user.groups.length">
|
<span *ngIf="user.groups && user.groups.length">
|
||||||
<mat-icon>people</mat-icon>
|
<mat-icon>people</mat-icon>
|
||||||
{{ user.groups }}
|
{{ user.groups }}
|
||||||
@ -94,6 +97,13 @@
|
|||||||
<span translate>Groups</span>
|
<span translate>Groups</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div *ngIf="presenceViewConfigured">
|
||||||
|
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="presence">
|
||||||
|
<mat-icon>transfer_within_a_station</mat-icon>
|
||||||
|
<span translate>Presence</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button mat-menu-item (click)="csvExportUserList()">
|
<button mat-menu-item (click)="csvExportUserList()">
|
||||||
<mat-icon>archive</mat-icon>
|
<mat-icon>archive</mat-icon>
|
||||||
<span translate>Export as CSV</span>
|
<span translate>Export as CSV</span>
|
||||||
@ -103,7 +113,6 @@
|
|||||||
<mat-icon>save_alt</mat-icon>
|
<mat-icon>save_alt</mat-icon>
|
||||||
<span translate>Import</span><span> ...</span>
|
<span translate>Import</span><span> ...</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="isMultiSelect">
|
<div *ngIf="isMultiSelect">
|
||||||
<button mat-menu-item (click)="selectAll()">
|
<button mat-menu-item (click)="selectAll()">
|
||||||
|
@ -6,6 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
|
|
||||||
import { CsvExportService } from '../../../../core/services/csv-export.service';
|
import { CsvExportService } from '../../../../core/services/csv-export.service';
|
||||||
import { ChoiceService } from '../../../../core/services/choice.service';
|
import { ChoiceService } from '../../../../core/services/choice.service';
|
||||||
|
import { ConfigService } from 'app/core/services/config.service';
|
||||||
import { ListViewBaseComponent } from '../../../base/list-view-base';
|
import { ListViewBaseComponent } from '../../../base/list-view-base';
|
||||||
import { GroupRepositoryService } from '../../services/group-repository.service';
|
import { GroupRepositoryService } from '../../services/group-repository.service';
|
||||||
import { PromptService } from '../../../../core/services/prompt.service';
|
import { PromptService } from '../../../../core/services/prompt.service';
|
||||||
@ -24,6 +25,19 @@ import { UserSortListService } from '../../services/user-sort-list.service';
|
|||||||
styleUrls: ['./user-list.component.scss']
|
styleUrls: ['./user-list.component.scss']
|
||||||
})
|
})
|
||||||
export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit {
|
export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit {
|
||||||
|
/**
|
||||||
|
* Stores the observed configuration if the presence view is available to administrators
|
||||||
|
*/
|
||||||
|
private _presenceViewConfigured = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Does not check for user manage rights itself
|
||||||
|
* @returns true if the presence view is available to administrators
|
||||||
|
*/
|
||||||
|
public get presenceViewConfigured(): boolean {
|
||||||
|
return this._presenceViewConfigured;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /**
|
* /**
|
||||||
* The usual constructor for components
|
* The usual constructor for components
|
||||||
@ -39,6 +53,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
|||||||
* @param groupRepo
|
* @param groupRepo
|
||||||
* @param filterService
|
* @param filterService
|
||||||
* @param sortService
|
* @param sortService
|
||||||
|
* @param config ConfigService
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
titleService: Title,
|
titleService: Title,
|
||||||
@ -52,12 +67,14 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
|||||||
protected csvExport: CsvExportService,
|
protected csvExport: CsvExportService,
|
||||||
private promptService: PromptService,
|
private promptService: PromptService,
|
||||||
public filterService: UserFilterListService,
|
public filterService: UserFilterListService,
|
||||||
public sortService: UserSortListService
|
public sortService: UserSortListService,
|
||||||
|
config: ConfigService
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar);
|
super(titleService, translate, matSnackBar);
|
||||||
|
|
||||||
// enable multiSelect for this listView
|
// enable multiSelect for this listView
|
||||||
this.canMultiSelect = true;
|
this.canMultiSelect = true;
|
||||||
|
config.get('users_enable_presence_view').subscribe(state => (this._presenceViewConfigured = state));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
<os-head-bar [mainButton]="false" [nav]="false">
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="title-slot"><h2 translate>Presence</h2></div>
|
||||||
|
</os-head-bar>
|
||||||
|
|
||||||
|
<mat-card *ngIf="permission">
|
||||||
|
<span translate> Check in or check out participants based on their participant numbers </span>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<mat-form-field [formGroup]="userForm">
|
||||||
|
<input osAutofocus
|
||||||
|
matInput
|
||||||
|
[formControl]="userForm.get('number')"
|
||||||
|
placeholder="{{ 'Enter participant number' | translate }}"
|
||||||
|
(keyup)="onKeyUp($event)"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
<button mat-button (click)="changePresence()">Change presence</button>
|
||||||
|
<mat-card *ngIf="lastChangedUser" [ngClass]="lastChangedUser.is_present ? 'success' : 'warning'">
|
||||||
|
<span>{{ lastChangedUser.full_name }} </span> <span translate> is now</span>
|
||||||
|
<span> {{ lastChangedUser.is_present ? 'present' : ('not present' | translate) }}</span>
|
||||||
|
</mat-card>
|
||||||
|
<mat-card *ngIf="errorMsg" class="error"> {{ errorMsg | translate }} </mat-card>
|
||||||
|
</mat-card>
|
@ -0,0 +1,26 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
import { PresenceDetailComponent } from './presence-detail.component';
|
||||||
|
|
||||||
|
describe('PresenceDetailComponent', () => {
|
||||||
|
let component: PresenceDetailComponent;
|
||||||
|
let fixture: ComponentFixture<PresenceDetailComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [PresenceDetailComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(PresenceDetailComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,133 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
import { ConfigService } from 'app/core/services/config.service';
|
||||||
|
import { OperatorService } from 'app/core/services/operator.service';
|
||||||
|
import { UserRepositoryService } from '../services/user-repository.service';
|
||||||
|
import { ViewUser } from '../models/view-user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component offers an input field for user numbers, and sets/unsets the
|
||||||
|
* 'is_present' status for the user associated with that number, giving a feedback
|
||||||
|
* by displaying the name and the new presence status of that user.
|
||||||
|
*
|
||||||
|
* The component is typically directly accessed via the router link
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'os-presence-detail',
|
||||||
|
templateUrl: './presence-detail.component.html'
|
||||||
|
})
|
||||||
|
export class PresenceDetailComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The form group for the input field
|
||||||
|
*/
|
||||||
|
public userForm: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the last user entered. Is null if there is no user or the last
|
||||||
|
* participant number has no unique valid user
|
||||||
|
*/
|
||||||
|
public lastChangedUser: ViewUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to update {@link lastChangedUser}
|
||||||
|
*/
|
||||||
|
private _userSubscription: Subscription = null;
|
||||||
|
public errorMsg: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config variable if this view is enabled in the config
|
||||||
|
* TODO: Should be a temporary check, until the permission on users-routing.module is fixed
|
||||||
|
*/
|
||||||
|
private _enabledInConfig: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* permission check if user is allowed to access this view.
|
||||||
|
* TODO: Should be a temporary check, until the permission on users-routing.module is fixed
|
||||||
|
*
|
||||||
|
* @returns true if the user is allowed to use this view
|
||||||
|
*/
|
||||||
|
public get permission(): boolean {
|
||||||
|
return this.operator.hasPerms('users.can_manage') && this._enabledInConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor. Subscribes to the configuration if this view should be enabled at all
|
||||||
|
*
|
||||||
|
* @param userRepo: UserRepositoryService for querying the users
|
||||||
|
* @param formBuilder FormBuilder input form
|
||||||
|
* @param operator OperatorService fetch the current user for a permission check
|
||||||
|
* @param config ConfigService checking if the feature is enabled
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private userRepo: UserRepositoryService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private operator: OperatorService,
|
||||||
|
config: ConfigService
|
||||||
|
) {
|
||||||
|
config.get('users_enable_presence_view').subscribe(conf => (this._enabledInConfig = conf));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initializes the form control
|
||||||
|
*/
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.userForm = this.formBuilder.group({
|
||||||
|
number: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the user finding and updating process. The user number will be taken from the {@link userForm}.
|
||||||
|
* Feedback will be relayed to the {@link errorMsg} and/or {@link lastChangedUser} variables
|
||||||
|
*/
|
||||||
|
public async changePresence(): Promise<void> {
|
||||||
|
const number = this.userForm.get('number').value;
|
||||||
|
const users = this.userRepo.getUsersByNumber(number);
|
||||||
|
this.userForm.reset();
|
||||||
|
if (users.length === 1) {
|
||||||
|
await this.userRepo.update({ is_present: !users[0].is_present }, users[0]);
|
||||||
|
this.subscribeUser(users[0].id);
|
||||||
|
} else if (!users.length) {
|
||||||
|
this.clearSubscription();
|
||||||
|
this.errorMsg = 'Participant cannot be found';
|
||||||
|
} else if (users.length > 1) {
|
||||||
|
this.clearSubscription();
|
||||||
|
this.errorMsg = 'Participant number is not unique';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes this component to a user given by an id. The
|
||||||
|
* {@link lastChangedUser} will be updated accordingly.
|
||||||
|
*
|
||||||
|
* @param id the id of the user to be shown as lastChangedUser
|
||||||
|
*/
|
||||||
|
private subscribeUser(id: number): void {
|
||||||
|
this.clearSubscription();
|
||||||
|
this.errorMsg = null;
|
||||||
|
this._userSubscription = this.userRepo
|
||||||
|
.getViewModelObservable(id)
|
||||||
|
.subscribe(user => (this.lastChangedUser = user));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the currently displayed user and subscription, if any is present
|
||||||
|
*/
|
||||||
|
private clearSubscription(): void {
|
||||||
|
if (this._userSubscription) {
|
||||||
|
this._userSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
this.lastChangedUser = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* triggers the submission on enter key
|
||||||
|
*/
|
||||||
|
public onKeyUp(event: KeyboardEvent): void {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
this.changePresence();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -213,7 +213,9 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches and returns Users by full name
|
* Searches and returns Users by full name
|
||||||
|
*
|
||||||
* @param name
|
* @param name
|
||||||
|
* @returns all users matching that name
|
||||||
*/
|
*/
|
||||||
public getUsersByName(name: string): ViewUser[] {
|
public getUsersByName(name: string): ViewUser[] {
|
||||||
const results: ViewUser[] = [];
|
const results: ViewUser[] = [];
|
||||||
@ -232,6 +234,16 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches and returns Users by participant number
|
||||||
|
*
|
||||||
|
* @param number: A participant number
|
||||||
|
* @returns all users matching that number
|
||||||
|
*/
|
||||||
|
public getUsersByNumber(number: string): ViewUser[] {
|
||||||
|
return this.getViewModelList().filter(user => user.participant_number === number);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new User from a string
|
* Creates a new User from a string
|
||||||
*
|
*
|
||||||
|
@ -3,6 +3,7 @@ import { Routes, RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { GroupListComponent } from './components/group-list/group-list.component';
|
import { GroupListComponent } from './components/group-list/group-list.component';
|
||||||
import { PasswordComponent } from './components/password/password.component';
|
import { PasswordComponent } from './components/password/password.component';
|
||||||
|
import { PresenceDetailComponent } from './presence-detail/presence-detail.component';
|
||||||
import { UserDetailComponent } from './components/user-detail/user-detail.component';
|
import { UserDetailComponent } from './components/user-detail/user-detail.component';
|
||||||
import { UserImportListComponent } from './components/user-import/user-import-list.component';
|
import { UserImportListComponent } from './components/user-import/user-import-list.component';
|
||||||
import { UserListComponent } from './components/user-list/user-list.component';
|
import { UserListComponent } from './components/user-list/user-list.component';
|
||||||
@ -28,6 +29,11 @@ const routes: Routes = [
|
|||||||
path: 'import',
|
path: 'import',
|
||||||
component: UserImportListComponent
|
component: UserImportListComponent
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'presence',
|
||||||
|
component: PresenceDetailComponent
|
||||||
|
// FIXME: CRITICAL: restricted to basePerm: 'users.can_manage' and config 'users_enable_presence_view'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'groups',
|
path: 'groups',
|
||||||
component: GroupListComponent
|
component: GroupListComponent
|
||||||
|
@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
|
|
||||||
import { GroupListComponent } from './components/group-list/group-list.component';
|
import { GroupListComponent } from './components/group-list/group-list.component';
|
||||||
import { PasswordComponent } from './components/password/password.component';
|
import { PasswordComponent } from './components/password/password.component';
|
||||||
|
import { PresenceDetailComponent } from './presence-detail/presence-detail.component';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { UserDetailComponent } from './components/user-detail/user-detail.component';
|
import { UserDetailComponent } from './components/user-detail/user-detail.component';
|
||||||
import { UserImportListComponent } from './components/user-import/user-import-list.component';
|
import { UserImportListComponent } from './components/user-import/user-import-list.component';
|
||||||
@ -16,7 +17,8 @@ import { UsersRoutingModule } from './users-routing.module';
|
|||||||
UserDetailComponent,
|
UserDetailComponent,
|
||||||
GroupListComponent,
|
GroupListComponent,
|
||||||
PasswordComponent,
|
PasswordComponent,
|
||||||
UserImportListComponent
|
UserImportListComponent,
|
||||||
|
PresenceDetailComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
Loading…
Reference in New Issue
Block a user