view for changing the user presence by oarticipant number

This commit is contained in:
Maximilian Krambach 2019-01-24 16:48:03 +01:00
parent 2c7e181f38
commit 8e086df440
8 changed files with 355 additions and 126 deletions

View File

@ -15,142 +15,151 @@
</os-head-bar>
<mat-drawer-container class="on-transition-fade">
<os-sort-filter-bar [sortService]="sortService" [filterService]="filterService"
(searchFieldChange)="searchFilter($event)">
</os-sort-filter-bar>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<!-- Selector column -->
<ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell"></mat-header-cell>
<mat-cell *matCellDef="let user" (click)="selectItem(user, $event)" class="icon-cell">
<mat-icon>{{ isSelected(user) ? 'check_circle' : '' }}</mat-icon>
</mat-cell>
</ng-container>
<!-- Projector column -->
<ng-container matColumnDef="projector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell">Projector</mat-header-cell>
<mat-cell *matCellDef="let user" class="icon-cell">
<os-projector-button [object]="user"></os-projector-button>
</mat-cell>
</ng-container>
<!-- name column -->
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
<mat-cell *matCellDef="let user" (click)="selectItem(user, $event)">{{ user.full_name }}</mat-cell>
</ng-container>
<!-- prefix column -->
<ng-container matColumnDef="group">
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
<mat-cell *matCellDef="let user">
<div class='groupsCell'>
<span *ngIf="user.groups && user.groups.length">
<mat-icon>people</mat-icon>
{{ user.groups }}
</span>
<br *ngIf="user.groups && user.structureLevel" />
<span *ngIf="user.structureLevel">
<mat-icon>flag</mat-icon>
{{ user.structure_level }}
</span>
</div>
</mat-cell>
</ng-container>
<!-- Presence column -->
<ng-container matColumnDef="presence">
<mat-header-cell *matHeaderCellDef mat-sort-header>Presence</mat-header-cell>
<mat-cell *matCellDef="let user" class="presentCell">
<div *ngIf="user.is_active">
<mat-checkbox class="checkboxPresent" (click)="setPresent(user)" [checked]="user.is_present">
<span translate>Present</span>
</mat-checkbox>
</div>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
*matRowDef="let row; columns: getColumnDefinition()"
<os-sort-filter-bar
[sortService]="sortService"
[filterService]="filterService"
(searchFieldChange)="searchFilter($event)"
>
</mat-row>
</mat-table>
</os-sort-filter-bar>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<!-- Selector column -->
<ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell"></mat-header-cell>
<mat-cell *matCellDef="let user" (click)="selectItem(user, $event)" class="icon-cell">
<mat-icon>{{ isSelected(user) ? 'check_circle' : '' }}</mat-icon>
</mat-cell>
</ng-container>
<mat-menu #userMenu="matMenu">
<div *ngIf="!isMultiSelect">
<button mat-menu-item *osPerms="'users.can_manage'" (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon>
<span translate>Multiselect</span>
</button>
<!-- Projector column -->
<ng-container matColumnDef="projector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell">Projector</mat-header-cell>
<mat-cell *matCellDef="let user" class="icon-cell">
<os-projector-button [object]="user"></os-projector-button>
</mat-cell>
</ng-container>
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="groups">
<mat-icon>people</mat-icon>
<span translate>Groups</span>
</button>
<!-- name column -->
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
<mat-cell *matCellDef="let user" (click)="selectItem(user, $event)">{{ user.full_name }}</mat-cell>
</ng-container>
<button mat-menu-item (click)="csvExportUserList()">
<mat-icon>archive</mat-icon>
<span translate>Export as CSV</span>
</button>
<!-- prefix column -->
<ng-container matColumnDef="group">
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
<mat-cell *matCellDef="let user">
<div class="groupsCell">
<span *ngIf="user.groups && user.groups.length">
<mat-icon>people</mat-icon>
{{ user.groups }}
</span>
<br *ngIf="user.groups && user.structureLevel" />
<span *ngIf="user.structureLevel">
<mat-icon>flag</mat-icon>
{{ user.structure_level }}
</span>
</div>
</mat-cell>
</ng-container>
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="import">
<mat-icon>save_alt</mat-icon>
<span translate>Import</span><span>&nbsp;...</span>
</button>
<!-- Presence column -->
<ng-container matColumnDef="presence">
<mat-header-cell *matHeaderCellDef mat-sort-header>Presence</mat-header-cell>
<mat-cell *matCellDef="let user" class="presentCell">
<div *ngIf="user.is_active">
<mat-checkbox class="checkboxPresent" (click)="setPresent(user)" [checked]="user.is_present">
<span translate>Present</span>
</mat-checkbox>
</div>
</mat-cell>
</ng-container>
</div>
<div *ngIf="isMultiSelect">
<button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon>
<span translate>Select all</span>
</button>
<button mat-menu-item (click)="deselectAll()">
<mat-icon>clear</mat-icon>
<span translate>Deselect all</span>
</button>
<div *osPerms="'users.can_manage'">
<mat-divider></mat-divider>
<button mat-menu-item (click)="setGroupSelected()">
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row
[ngClass]="selectedRows.indexOf(row) >= 0 ? 'selected' : ''"
*matRowDef="let row; columns: getColumnDefinition()"
>
</mat-row>
</mat-table>
<mat-paginator class="on-transition-fade" [pageSizeOptions]="[25, 50, 75, 100, 125]"></mat-paginator>
<mat-menu #userMenu="matMenu">
<div *ngIf="!isMultiSelect">
<button mat-menu-item *osPerms="'users.can_manage'" (click)="toggleMultiSelect()">
<mat-icon>library_add</mat-icon>
<span translate>Multiselect</span>
</button>
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="groups">
<mat-icon>people</mat-icon>
<span translate>Add/remove groups ...</span>
<span translate>Groups</span>
</button>
<button mat-menu-item (click)="setActiveSelected()">
<mat-icon>block</mat-icon>
<span translate>Enable/disable account ...</span>
<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()">
<mat-icon>archive</mat-icon>
<span translate>Export as CSV</span>
</button>
<button mat-menu-item (click)="setPresentSelected()">
<mat-icon>check_box</mat-icon>
<span translate>Set presence ...</span>
</button>
<button mat-menu-item (click)="setCommitteeSelected()">
<mat-icon>account_balance</mat-icon>
<span translate>Set committee ...</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="sendInvitationEmailSelected()">
<mat-icon>mail</mat-icon>
<span translate>Send invitation email</span>
</button>
<button mat-menu-item (click)="resetPasswordsSelected()">
<mat-icon>vpn_key</mat-icon>
<span translate>Generate new passwords</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="red-warning-text" (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
<span translate>Delete</span>
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="import">
<mat-icon>save_alt</mat-icon>
<span translate>Import</span><span>&nbsp;...</span>
</button>
</div>
</div>
</mat-menu>
<div *ngIf="isMultiSelect">
<button mat-menu-item (click)="selectAll()">
<mat-icon>done_all</mat-icon>
<span translate>Select all</span>
</button>
<button mat-menu-item (click)="deselectAll()">
<mat-icon>clear</mat-icon>
<span translate>Deselect all</span>
</button>
<div *osPerms="'users.can_manage'">
<mat-divider></mat-divider>
<button mat-menu-item (click)="setGroupSelected()">
<mat-icon>people</mat-icon>
<span translate>Add/remove groups ...</span>
</button>
<button mat-menu-item (click)="setActiveSelected()">
<mat-icon>block</mat-icon>
<span translate>Enable/disable account ...</span>
</button>
<button mat-menu-item (click)="setPresentSelected()">
<mat-icon>check_box</mat-icon>
<span translate>Set presence ...</span>
</button>
<button mat-menu-item (click)="setCommitteeSelected()">
<mat-icon>account_balance</mat-icon>
<span translate>Set committee ...</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="sendInvitationEmailSelected()">
<mat-icon>mail</mat-icon>
<span translate>Send invitation email</span>
</button>
<button mat-menu-item (click)="resetPasswordsSelected()">
<mat-icon>vpn_key</mat-icon>
<span translate>Generate new passwords</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="red-warning-text" (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
<span translate>Delete</span>
</button>
</div>
</div>
</mat-menu>
</mat-drawer-container>

View File

@ -6,6 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CsvExportService } from '../../../../core/services/csv-export.service';
import { ChoiceService } from '../../../../core/services/choice.service';
import { ConfigService } from 'app/core/services/config.service';
import { ListViewBaseComponent } from '../../../base/list-view-base';
import { GroupRepositoryService } from '../../services/group-repository.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']
})
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
@ -39,6 +53,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
* @param groupRepo
* @param filterService
* @param sortService
* @param config ConfigService
*/
public constructor(
titleService: Title,
@ -52,12 +67,14 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
protected csvExport: CsvExportService,
private promptService: PromptService,
public filterService: UserFilterListService,
public sortService: UserSortListService
public sortService: UserSortListService,
config: ConfigService
) {
super(titleService, translate, matSnackBar);
// enable multiSelect for this listView
this.canMultiSelect = true;
config.get('users_enable_presence_view').subscribe(state => (this._presenceViewConfigured = state));
}
/**

View File

@ -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 }}&nbsp;</span> <span translate> is now</span>
<span>&nbsp;{{ lastChangedUser.is_present ? 'present' : ('not present' | translate) }}</span>
</mat-card>
<mat-card *ngIf="errorMsg" class="error"> {{ errorMsg | translate }} </mat-card>
</mat-card>

View File

@ -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();
});
});

View File

@ -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();
}
}
}

View File

@ -213,7 +213,9 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
/**
* Searches and returns Users by full name
*
* @param name
* @returns all users matching that name
*/
public getUsersByName(name: string): ViewUser[] {
const results: ViewUser[] = [];
@ -232,6 +234,16 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
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
*

View File

@ -3,6 +3,7 @@ import { Routes, RouterModule } from '@angular/router';
import { GroupListComponent } from './components/group-list/group-list.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 { UserImportListComponent } from './components/user-import/user-import-list.component';
import { UserListComponent } from './components/user-list/user-list.component';
@ -28,6 +29,11 @@ const routes: Routes = [
path: 'import',
component: UserImportListComponent
},
{
path: 'presence',
component: PresenceDetailComponent
// FIXME: CRITICAL: restricted to basePerm: 'users.can_manage' and config 'users_enable_presence_view'
},
{
path: 'groups',
component: GroupListComponent

View File

@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
import { GroupListComponent } from './components/group-list/group-list.component';
import { PasswordComponent } from './components/password/password.component';
import { PresenceDetailComponent } from './presence-detail/presence-detail.component';
import { SharedModule } from '../../shared/shared.module';
import { UserDetailComponent } from './components/user-detail/user-detail.component';
import { UserImportListComponent } from './components/user-import/user-import-list.component';
@ -16,7 +17,8 @@ import { UsersRoutingModule } from './users-routing.module';
UserDetailComponent,
GroupListComponent,
PasswordComponent,
UserImportListComponent
UserImportListComponent,
PresenceDetailComponent
]
})
export class UsersModule {}