Rework admin user management to use default two culumn layout (CC-68)

pull/47/head
HardiReady 2018-11-24 14:05:52 +01:00
parent 72d3cac89f
commit 118214a906
22 changed files with 486 additions and 198 deletions

View File

@ -15,15 +15,17 @@ const account = new express.Router();
account.route('/')
.get((req, res, next) => {
AppUserModel.find({}, {}, {sort: {username: 1}}).populate('squad').exec((err, items) => {
if (err) {
err.status = codes.servererror;
return next(err);
}
res.locals.items = items;
res.locals.processed = true;
next();
});
AppUserModel.find({}, {}, {sort: {username: 1}})
.populate('squad')
.exec((err, items) => {
if (err) {
err.status = codes.servererror;
return next(err);
}
res.locals.items = items;
res.locals.processed = true;
next();
});
})
.all(
routerHandling.httpMethodNotAllowed
@ -31,33 +33,45 @@ account.route('/')
// routes **********************
account.route('/:id')
.get((req, res, next) => {
AppUserModel.findById(req.params.id)
.populate('squad')
.exec((err, item) => {
if (err) {
err.status = codes.servererror;
} else if (!item) {
err = new Error('item not found');
err.status = codes.notfound;
}
res.locals.items = item;
next(err);
});
})
.patch((req, res, next) => {
if (!req.body || (req.body._id && req.body._id !== req.params.id)) {
// little bit different as in PUT. :id does not need to be in data, but if the _id and url id must match
const err = new Error('id of PATCH resource and send JSON body are not equal ' + req.params.id + ' ' +
req.body._id);
const err = new Error(
'id of PATCH resource and send JSON body are not equal ' + req.params.id + ' ' + req.body._id);
err.status = codes.notfound;
next(err);
return; // prevent node to process this function further after next() has finished.
return next(err);
}
// increment version manually as we do not use .save(.)
req.body.updatedAt = new Date();
req.body.$inc = {__v: 1};
// PATCH is easier with mongoose than PUT. You simply update by all data that comes from outside. no need to
// reset attributes that are missing.
AppUserModel.findByIdAndUpdate(req.params.id, req.body, {new: true}).populate('squad').exec((err, item) => {
if (err) {
err.status = codes.wrongrequest;
} else if (!item) {
err = new Error('appUser not found');
err.status = codes.notfound;
} else {
res.locals.items = item;
}
next(err);
});
AppUserModel.findByIdAndUpdate(req.params.id, req.body, {new: true})
.populate('squad')
.exec((err, item) => {
if (err) {
err.status = codes.wrongrequest;
} else if (!item) {
err = new Error('appUser not found');
err.status = codes.notfound;
} else {
res.locals.items = item;
}
next(err);
});
})
.delete((req, res, next) => {
@ -68,10 +82,8 @@ account.route('/:id')
err = new Error('item not found');
err.status = codes.notfound;
}
// we don't set res.locals.items and thus it will send a 204 (no content) at the end. see last handler
// user.use(..)
res.locals.processed = true;
next(err); // this works because err is in normal case undefined and that is the same as no parameter
next(err);
});
})
@ -79,8 +91,6 @@ account.route('/:id')
routerHandling.httpMethodNotAllowed
);
// this middleware function can be used, if you like or remove it
// it looks for object(s) in res.locals.items and if they exist, they are send to the client as json
account.use(routerHandling.emptyResponse);
module.exports = account;

View File

@ -1,27 +0,0 @@
.overview {
padding-bottom: 50px!important;
}
.trash {
cursor: pointer;
}
.table {
overflow-wrap: break-word;
table-layout: fixed;
}
.table-container {
margin-top: 10px;
overflow-x: auto;
padding: 5px;
}
.table-head {
background: #222222;
color: white;
}
.cell-outline {
outline: 1px solid #D4D4D4;
}

View File

@ -1,65 +1 @@
<div class="overview">
<h2>Admin Panel</h2>
<div class="pull-left" style="margin-top:20px;">
<div class="table-container" style="width: 75%; min-width: 500px">
<table class="table table-hover">
<thead>
<tr class="table-head">
<th class="col-sm-1" style="border-radius: 10px 0 0 0;">Username</th>
<th class="col-sm-1">Activated</th>
<th class="col-sm-1">Secret</th>
<th class="col-sm-1">Fraktion/ Squad</th>
<th class="col-sm-1">Permission</th>
<th class="col-sm-1 text-center" style="border-radius: 0 10px 0 0;"></th>
</tr>
</thead>
<tbody *ngFor="let user of users$ | async">
<tr class="cell-outline">
<td>
{{user.username}}
</td>
<td style="font-weight: bold">
<select id="activated" name="activated" class="form-control btn dropdown-toggle"
[(ngModel)]="user.activated" (change)="updateAppUser(user)">
<option value="true">activated</option>
<option value="false">deactivated</option>
</select>
</td>
<td>
{{user.secret}}
</td>
<td>
<select class="form-control"
name="squad"
id="squad"
[(ngModel)]="user.squad"
[compareWith]="equals"
(change)="updateAppUser(user)">
<option [value]="0">Ohne Fraktion/ Squad</option>
<option *ngFor="let squad of squads" [ngValue]="squad">
{{squad.fraction == 'BLUFOR'? fraction.BLUFOR : fraction.OPFOR}}: {{squad.name}}
</option>
</select>
</td>
<td>
<select id="permission" name="permission" class="form-control btn dropdown-toggle"
[(ngModel)]="user.permission" (change)="updateAppUser(user)">
<option value="0">User</option>
<option value="1">SQL</option>
<option value="2">HL</option>
<option value="3">MT</option>
<option value="4">Admin</option>
</select>
</td>
<td class="text-center">
<span class="glyphicon glyphicon-trash trash" matTooltip="Löschen" (click)="deleteUser(user)"></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<router-outlet></router-outlet>

View File

@ -1,68 +1,11 @@
import {Component, OnInit} from '@angular/core';
import {AppUser, Squad} from '../models/model-interfaces';
import {Observable} from 'rxjs/Observable';
import {AppUserService} from '../services/app-user-service/app-user.service';
import {SquadService} from '../services/army-management/squad.service';
import {Fraction} from '../utils/fraction.enum';
import {SnackBarService} from '../services/user-interface/snack-bar/snack-bar.service';
import {Component} from '@angular/core';
@Component({
selector: 'admin-panel',
selector: 'cc-admin-component',
templateUrl: './admin.component.html',
styleUrls: ['./admin.component.css', '../style/overview.css']
})
export class AdminComponent implements OnInit {
users$: Observable<AppUser[]>;
squads: Squad[] = [];
readonly fraction = Fraction;
constructor(private appUserService: AppUserService,
private squadService: SquadService,
private snackBarService: SnackBarService) {
}
ngOnInit() {
this.users$ = this.appUserService.getUsers();
this.squadService.findSquads().subscribe(squads => {
this.squads = squads;
});
}
updateAppUser(user) {
const updateObject = {
_id: user._id,
squad: user.squad,
activated: user.activated,
permission: user.permission
};
if (updateObject.squad === '0') {
updateObject.squad = null;
}
this.appUserService.updateUser(updateObject)
.subscribe(resUser => {
this.snackBarService.showSuccess('generic.save.success');
});
}
deleteUser(user) {
if (confirm('Soll der Nutzer "' + user.username + '" wirklich gelöscht werden?')) {
this.appUserService.deleteUser(user)
.subscribe((res) => {
});
}
}
/**
* compare ngValue with ngModel to assign selected element
*/
equals(o1: Squad, o2: Squad) {
if (o1 && o2) {
return o1._id === o2._id;
}
export class AdminComponent {
constructor() {
}
}

View File

@ -1,14 +1,36 @@
import {NgModule} from '@angular/core';
import {AdminComponent} from './admin.component';
import {SharedModule} from '../shared.module';
import {AppUserService} from '../services/app-user-service/app-user.service';
import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router';
import {adminRouterModule, adminRoutingComponents} from './admin.routing';
import {HttpClient} from '@angular/common/http';
import {TranslateHttpLoader} from '@ngx-translate/http-loader';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/admin/', '.json');
}
@NgModule({
declarations: [AdminComponent],
imports: [CommonModule, SharedModule, RouterModule.forChild([{path: '', component: AdminComponent}])],
declarations: adminRoutingComponents,
imports: [
CommonModule,
SharedModule,
adminRouterModule,
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: [HttpClient]
},
isolate: true
})
],
providers: [AppUserService]
})
export class AdminModule {
static routes = adminRouterModule;
}

View File

@ -0,0 +1,37 @@
import {RouterModule, Routes} from '@angular/router';
import {ModuleWithProviders} from '@angular/core';
import {AdminComponent} from './admin.component';
import {AppUserListComponent} from './user-list/app-user-list.component';
import {EditAppUserComponent} from './edit-app-user/edit-app-user.component';
import {AppUserItemComponent} from './user-list/app-user-item.component';
export const adminRoutes: Routes = [
{
path: 'users',
children: [
{
path: '',
component: AdminComponent,
outlet: 'left',
children: [{
path: '',
component: AppUserListComponent
}]
},
{
path: 'new',
component: EditAppUserComponent,
outlet: 'right'
},
{
path: 'edit/:id',
component: EditAppUserComponent,
outlet: 'right'
}
]
},
];
export const adminRouterModule: ModuleWithProviders = RouterModule.forChild(adminRoutes);
export const adminRoutingComponents = [AdminComponent, AppUserListComponent, AppUserItemComponent, EditAppUserComponent];

View File

@ -0,0 +1,61 @@
<form #form="ngForm" (keydown.enter)="$event.preventDefault()"
class="overview"
style="display: flex;flex-direction: column;">
<h3 *ngIf="appUser._id">{{'user.submit.headline.edit' | translate}}</h3>
<h3 *ngIf="!appUser._id">{{'user.submit.headline.new' | translate}}</h3>
<mat-form-field>
<label for="title">{{'user.submit.field.name' | translate}}</label>
<input matInput
[(ngModel)]="appUser.username"
name="title"
id="title"
required
maxlength="50"/>
<show-error displayName="{{'user.submit.field.name' | translate}}" controlPath="title"></show-error>
</mat-form-field>
<div class="form-group">
<label for="squad">{{'user.submit.field.squad' | translate}}</label>
<select class="form-control"
name="squad"
id="squad"
[(ngModel)]="appUserSquadId">
<option [value]="null">{{'user.submit.field.squad.not.assigned' | translate}}</option>
<option *ngFor="let squad of squads" [ngValue]="squad._id">
{{squad.fraction == 'BLUFOR'? fraction.BLUFOR : fraction.OPFOR}}: {{squad.name}}
</option>
</select>
<show-error displayName="{{'user.submit.field.squad' | translate}}" controlPath="squad"></show-error>
</div>
<label>{{'user.submit.field.activated' | translate}}</label>
<mat-slide-toggle name="activated" [(ngModel)]="appUser.activated"></mat-slide-toggle>
<label>{{'user.submit.field.permission' | translate}}</label>
<mat-form-field>
<mat-select name="permission" [(ngModel)]="appUser.permission">
<mat-option [value]="0">User</mat-option>
<mat-option [value]="1">SQL</mat-option>
<mat-option [value]="2">HL</mat-option>
<mat-option [value]="3">MT</mat-option>
<mat-option [value]="4">Admin</mat-option>
</mat-select>
</mat-form-field>
<button id="cancel"
style="width: 50%"
(click)="cancel()"
class="btn btn-default">
{{'user.submit.button.cancel' | translate}}
</button>
<button id="save"
style="width: 50%"
type="submit"
(click)="saveUser()"
class="btn btn-default"
[disabled]="!form.valid">
{{'user.submit.button.submit' | translate}}
</button>
</form>

View File

@ -0,0 +1,88 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {AppUser, Rank, Squad} from '../../models/model-interfaces';
import {SquadService} from '../../services/army-management/squad.service';
import {Subscription} from 'rxjs/Subscription';
import {NgForm} from '@angular/forms';
import {Fraction} from '../../utils/fraction.enum';
import {SnackBarService} from '../../services/user-interface/snack-bar/snack-bar.service';
import {AppUserService} from '../../services/app-user-service/app-user.service';
@Component({
templateUrl: './edit-app-user.component.html',
styleUrls: ['./edit-app-user.component.css', '../../style/entry-form.css', '../../style/overview.css'],
})
export class EditAppUserComponent implements OnInit {
@ViewChild(NgForm) form: NgForm;
subscription: Subscription;
appUser: AppUser = {};
appUserSquadId;
squads: Squad[] = [];
ranks: Rank[] = [];
ranksDisplay = 'none';
error: string;
readonly fraction = Fraction;
constructor(private router: Router,
private route: ActivatedRoute,
private appUserService: AppUserService,
private squadService: SquadService,
private snackBarService: SnackBarService) {
}
ngOnInit() {
this.subscription = this.route.params
.map(params => params['id'])
.filter(id => id !== undefined)
.flatMap(id => this.appUserService.getAppUser(id))
.subscribe(appUser => {
this.appUser = appUser;
this.appUserSquadId = appUser.squad ? appUser.squad._id : 'null';
});
this.squadService.findSquads().subscribe(squads => {
this.squads = squads;
});
}
saveUser() {
const updateObject: AppUser = {
_id: this.appUser._id,
username: this.appUser.username,
squad: this.appUserSquadId === 'null' ? null : this.appUserSquadId,
activated: this.appUser.activated,
permission: this.appUser.permission,
};
this.appUserService.updateUser(updateObject)
.subscribe(appUser => {
this.appUser = appUser;
this.appUserSquadId = appUser.squad ? appUser.squad._id : 'null';
this.snackBarService.showSuccess('generic.save.success');
});
}
cancel() {
this.router.navigate([this.appUser._id ? '../..' : '..'], {relativeTo: this.route});
return false;
}
/**
* compare ngValue with ngModel to assign selected element
*/
equals(o1: Squad, o2: Squad) {
if (o1 && o2) {
return o1._id === o2._id;
}
}
}

View File

@ -0,0 +1,6 @@
.icon-award {
width: 27px;
height: 42px;
display: block;
margin-right: 25px;
}

View File

@ -0,0 +1,19 @@
<div class="fade-in list-entry" [ngClass]="{selected : selected}" (click)="select()">
<div class="row">
<div class="col-sm-8">
<span>
<a>{{appUser.username}}</a>
</span>
<br>
<small *ngIf="appUser.squad && appUser.squad.fraction == 'OPFOR'">{{fraction.OPFOR}} - {{appUser.squad.name}}</small>
<small *ngIf="appUser.squad && appUser.squad.fraction == 'BLUFOR'">{{fraction.BLUFOR}} - {{appUser.squad.name}}</small>
<small *ngIf="!appUser.squad">{{'users.list.item.label.no.squad' | translate}}</small>
</div>
<div class="col-sm-4">
<mat-icon (click)="delete(); $event.stopPropagation()" matTooltip="{{'users.list.tooltip.delete' | translate}}"
class="pull-right" style="margin-top: 8px;" svgIcon="delete"></mat-icon>
</div>
</div>
</div>

View File

@ -0,0 +1,38 @@
import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core';
import {AppUser} from '../../models/model-interfaces';
import {Fraction} from '../../utils/fraction.enum';
@Component({
selector: 'cc-app-user-item',
templateUrl: './app-user-item.component.html',
styleUrls: ['./app-user-item.component.css', '../../style/list-entry.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppUserItemComponent {
@Input() appUser: AppUser;
@Input() selected: boolean;
@Output() userSelected = new EventEmitter();
@Output() userAward = new EventEmitter();
@Output() userDelete = new EventEmitter();
readonly fraction = Fraction;
constructor() {
}
select() {
this.userSelected.emit(this.appUser._id);
}
award() {
this.userAward.emit(this.appUser._id);
}
delete() {
this.userDelete.emit(this.appUser);
}
}

View File

@ -0,0 +1,8 @@
<div class="select-list">
<cc-app-user-item *ngFor="let user of appUsers$ | async"
[appUser]="user"
(userDelete)="deleteUser(user)"
(userSelected)="selectUser($event)"
[selected]="user._id == selectedUserId">
</cc-app-user-item>
</div>

View File

@ -0,0 +1,71 @@
import {Component} from '@angular/core';
import {FormControl} from '@angular/forms';
import {ActivatedRoute, Router} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import {AppUser, Squad} from '../../models/model-interfaces';
import {Fraction} from '../../utils/fraction.enum';
import {MatButtonToggleGroup} from '@angular/material';
import {UIHelpers} from '../../utils/global.helpers';
import {TranslateService} from '@ngx-translate/core';
import {AppUserService} from '../../services/app-user-service/app-user.service';
import {SquadService} from '../../services/army-management/squad.service';
@Component({
selector: 'cc-app-user-list',
templateUrl: './app-user-list.component.html',
styleUrls: ['./app-user-list.component.css', '../../style/select-list.css']
})
export class AppUserListComponent {
selectedUserId: string | number = null;
appUsers$: Observable<AppUser[]>;
readonly fraction = Fraction;
searchTerm = new FormControl();
radioModel = '';
constructor(private appUserService: AppUserService,
private squadService: SquadService,
private router: Router,
private route: ActivatedRoute,
private translate: TranslateService) {
this.appUsers$ = this.appUserService.getUsers();
}
initObservable(observables: any) {
Observable.merge(observables.params as Observable<string>, observables.searchTerm)
.distinctUntilChanged()
.switchMap(query => this.filterAppUsers())
.subscribe();
}
openNewUserForm() {
this.selectedUserId = null;
this.router.navigate([{outlets: {'right': ['new']}}], {relativeTo: this.route});
}
selectUser(userId: string) {
this.selectedUserId = userId;
this.router.navigate([{outlets: {'right': ['edit', userId]}}], {relativeTo: this.route});
}
deleteUser(user: AppUser) {
this.translate.get('users.list.delete.confirm', {name: user.username}).subscribe((confirmQuestion) => {
if (confirm(confirmQuestion)) {
this.appUserService.deleteUser(user)
.subscribe((res) => {
});
}
});
}
filterAppUsers(group?: MatButtonToggleGroup) {
this.radioModel = UIHelpers.toggleReleaseButton(this.radioModel, group);
// TODO: Add filter attribute submit
return this.appUsers$ = this.appUserService.getUsers();
}
}

View File

@ -89,8 +89,18 @@
</ul>
<ul class="nav navbar-nav pull-right">
<li *ngIf="loginService.hasPermission(4)" routerLinkActive="active">
<a routerLink='{{config.adminPanelPath}}' class="link">{{'navigation.top.admin' | translate}}</a>
<li *ngIf="loginService.hasPermission(4)"
class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
aria-expanded="false">
{{'navigation.top.admin' | translate}}
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li routerLinkActive="active">
<a routerLink='{{config.adminPanelAppUsersPath}}' class="link">{{'navigation.top.management.users' | translate}}</a>
</li>
</ul>
</li>
<li *ngIf="loginService.isLoggedIn()" class="link" style="cursor: pointer">
<a (click)="logout()">{{'navigation.top.logout' | translate}}</a>

View File

@ -21,6 +21,7 @@ export class AppConfig {
export const RouteConfig = {
adminPanelPath: 'admin-panel',
adminPanelAppUsersPath: 'admin-panel/users',
managePath: 'manage',
manageDecorationPath: 'manage/decorations',
manageRankPath: 'manage/ranks',

View File

@ -3,10 +3,10 @@ import {Observable} from 'rxjs';
export interface AppUser {
_id?: string;
username?: string;
squad?: Squad;
squad?: any; // Squad or id-string
secret?: string;
activated: boolean;
permission: number;
activated?: boolean;
permission?: number;
token?: string;
}

View File

@ -17,7 +17,7 @@ export class AppUserService {
this.users$ = this.appUserStore.items$;
}
getUsers() {
getUsers(): Observable<AppUser[]> {
this.httpGateway.get<AppUser[]>(this.config.apiAppUserPath)
.do((users) => {
this.appUserStore.dispatch({type: LOAD, data: users});
@ -27,7 +27,11 @@ export class AppUserService {
return this.users$;
}
updateUser(user: AppUser) {
getAppUser(id: string): Observable<AppUser> {
return this.httpGateway.get<AppUser>(this.config.apiAppUserPath + id);
}
updateUser(user: AppUser): Observable<AppUser> {
return this.httpGateway.patch<AppUser>(this.config.apiAppUserPath + user._id, user)
.do(savedUser => {
const action = {type: EDIT, data: savedUser};

View File

@ -4,7 +4,8 @@ import {ShowErrorComponent} from './common/show-error/show-error.component';
import {CommonModule} from '@angular/common';
import {ListFilterComponent} from './common/user-interface/list-filter/list-filter.component';
import {SearchFieldComponent} from './common/user-interface/search-field/search-field.component';
import {MatButtonToggleModule, MatTooltipModule} from '@angular/material';
import {MatButtonToggleModule, MatTooltipModule, MatSlideToggleModule, MatFormFieldModule, MatOptionModule, MatSelectModule,
MatInputModule} from '@angular/material';
import {MatButtonModule} from '@angular/material/button';
import {MatIconModule} from '@angular/material/icon';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
@ -30,6 +31,11 @@ export function createTranslateLoader(http: HttpClient) {
MatButtonModule,
MatIconModule,
MatTooltipModule,
MatSlideToggleModule,
MatFormFieldModule,
MatOptionModule,
MatSelectModule,
MatInputModule,
TranslateModule.forRoot({
loader: {
@ -46,6 +52,11 @@ export function createTranslateLoader(http: HttpClient) {
MatButtonToggleModule,
MatButtonModule,
MatIconModule,
MatSlideToggleModule,
MatFormFieldModule,
MatOptionModule,
MatSelectModule,
MatInputModule,
ShowErrorComponent,
ListFilterComponent,
SearchFieldComponent,

View File

@ -0,0 +1,25 @@
{
"public.error.message.required": "{{fieldName}} ist ein Pflichtfeld",
"public.error.message.min.length": "{{fieldName}} muss mindestens {{boundary}} Zeichen enthalten",
"public.error.message.max.length": "{{fieldName}} darf maximal {{boundary}} Zeichen enthalten",
"public.error.message.email": "Bitte geben Sie eine gültige E-Mail Adresse an",
"public.error.message.no.user": "Der eingetragene Benutzer existiert nicht.",
"public.error.message.default": "{{fieldName}} ist nicht valide",
"public.common.search.button": "Suchen",
"users.list.tooltip.new": "Neuen Teilnehmer hinzufügen",
"users.list.tooltip.delete": "Löschen",
"users.list.filter.no.squad": "Ohne Squad",
"users.list.item.label.no.squad": "ohne Squad/Fraktion",
"users.list.delete.confirm": "Soll der Teilnehmer '{{name}}' wirklich gelöscht werden?",
"user.submit.headline.new": "Neuen Teilnehmer hinzufügen",
"user.submit.headline.edit": "Teilnehmer bearbeiten",
"user.submit.field.name": "Name",
"user.submit.field.squad": "Squad",
"user.submit.field.squad.not.assigned": "Ohne Fraktion/ Squad",
"user.submit.field.activated": "Aktiviert",
"user.submit.field.permission": "Zugriffsrechte",
"user.submit.button.submit": "Bestätigen",
"user.submit.button.cancel": "Abbrechen"
}

View File

@ -0,0 +1,25 @@
{
"public.error.message.required": "{{fieldName}} ist ein Pflichtfeld",
"public.error.message.min.length": "{{fieldName}} muss mindestens {{boundary}} Zeichen enthalten",
"public.error.message.max.length": "{{fieldName}} darf maximal {{boundary}} Zeichen enthalten",
"public.error.message.email": "Bitte geben Sie eine gültige E-Mail Adresse an",
"public.error.message.no.user": "Der eingetragene Benutzer existiert nicht.",
"public.error.message.default": "{{fieldName}} ist nicht valide",
"public.common.search.button": "Suchen",
"users.list.tooltip.new": "Neuen Teilnehmer hinzufügen",
"users.list.tooltip.delete": "Löschen",
"users.list.filter.no.squad": "Ohne Squad",
"users.list.item.label.no.squad": "ohne Squad/Fraktion",
"users.list.delete.confirm": "Soll der Teilnehmer '{{name}}' wirklich gelöscht werden?",
"user.submit.headline.new": "Neuen Teilnehmer hinzufügen",
"user.submit.headline.edit": "Teilnehmer bearbeiten",
"user.submit.field.name": "Name",
"user.submit.field.squad": "Squad",
"user.submit.field.squad.not.assigned": "Ohne Fraktion/ Squad",
"user.submit.field.activated": "Aktiviert",
"user.submit.field.permission": "Zugriffsrechte",
"user.submit.button.submit": "Bestätigen",
"user.submit.button.cancel": "Abbrechen"
}