Add request routes & ui for promotion

pull/1/head
Florian Hartwich 2017-06-10 13:16:15 +02:00
parent 609395febc
commit 6b7b90d174
16 changed files with 488 additions and 117 deletions

View File

@ -13,15 +13,25 @@ const PromotionSchema = new Schema({
ref: 'AppUser', ref: 'AppUser',
required: true required: true
}, },
rankLvl: { oldRankLvl: {
type: Number,
get: v => Math.round(v),
set: v => Math.round(v),
required: true
},
newRankLvl: {
type: Number, type: Number,
get: v => Math.round(v), get: v => Math.round(v),
set: v => Math.round(v), set: v => Math.round(v),
required: true required: true
}, },
confirmed: { confirmed: {
type: Boolean, type: Number,
default: true get: v => Math.round(v),
set: v => Math.round(v),
min: 0,
max: 2,
required: true
} }
}, { }, {
collection: 'promotion', collection: 'promotion',

View File

@ -47,7 +47,7 @@ authenticate.route('/')
let authCheck = (username, password, res) => { let authCheck = (username, password, res) => {
const deferred = Q.defer(); const deferred = Q.defer();
AppUserModel.findOne({username: username}, (err, user) => { AppUserModel.findOne({username: username}).populate('squad').exec((err, user) => {
if (err) deferred.reject(err.name + ': ' + err.message); if (err) deferred.reject(err.name + ': ' + err.message);
const diff = 3 * 60 * 24; // time till expiration [minutes] const diff = 3 * 60 * 24; // time till expiration [minutes]

View File

@ -10,7 +10,20 @@ const codes = require('./http-codes');
const routerHandling = require('../middleware/router-handling'); const routerHandling = require('../middleware/router-handling');
// Mongoose Model using mongoDB // Mongoose Model using mongoDB
const UserModel = require('../models/user');
const AwardingModel = require('../models/awarding'); const AwardingModel = require('../models/awarding');
const PromotionModel = require('../models/promotion');
// result set for proposer(appUser) population
const resultSet = {
'__v': 0,
'updatedAt': 0,
'timestamp': 0,
'password': 0,
'permission': 0,
'secret': 0,
'activated': 0
};
const request = express.Router(); const request = express.Router();
@ -41,6 +54,51 @@ request.route('/award')
request.route('/promotion') request.route('/promotion')
.get((req, res, next) => {
const squadFilter = req.query.squadId;
let userIds = [];
UserModel.find({squadId: squadFilter}, (err, items) => {
if (err) {
err.status = codes.servererror;
return next(err);
}
for (let item of items) {
userIds.push(item._id);
}
PromotionModel.find({userId: {"$in": userIds}}, {}, {sort: {timestamp: 'desc'}}).populate('userId').populate('proposer', resultSet).exec((err, items) => {
if (err) {
err.status = codes.servererror;
return next(err);
}
if (items && items.length > 0) {
res.locals.items = items;
} else {
res.locals.items = [];
}
res.locals.processed = true;
next();
})
});
})
.post((req, res, next) => {
const promotion = new PromotionModel(req.body);
promotion.confirmed = 0;
promotion.proposer = req.user._id;
// timestamp and default are set automatically by Mongoose Schema Validation
promotion.save((err) => {
if (err) {
err.status = codes.wrongrequest;
err.message += ' in fields: ' + Object.getOwnPropertyNames(err.errors);
return next(err);
}
res.status(codes.created);
res.locals.items = promotion;
next();
});
})
.all( .all(
routerHandling.httpMethodNotAllowed routerHandling.httpMethodNotAllowed

View File

@ -12,6 +12,7 @@ export class AppConfig {
public readonly apiSquadPath = '/squads/'; public readonly apiSquadPath = '/squads/';
public readonly apiUserPath = '/users/'; public readonly apiUserPath = '/users/';
public readonly apiOverviewPath = '/overview'; public readonly apiOverviewPath = '/overview';
public readonly apiRequestAwardPath = '/request/award' public readonly apiRequestAwardPath = '/request/award';
public readonly apiPromotionPath = '/request/promotion';
} }

View File

@ -29,6 +29,8 @@ import {ArmyService} from "./services/army-service/army.service";
import { ClipboardModule } from 'ngx-clipboard'; import { ClipboardModule } from 'ngx-clipboard';
import {AppUserService} from "./services/app-user-service/app-user.service"; import {AppUserService} from "./services/app-user-service/app-user.service";
import {AppUserStore} from "./services/stores/app-user.store"; import {AppUserStore} from "./services/stores/app-user.store";
import {PromotionService} from "./services/promotion-service/promotion.service";
import {FilterRankPipe} from "./filter/filter.pipe";
@NgModule({ @NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule, appRouting, HttpModule, ClipboardModule], imports: [BrowserModule, FormsModule, ReactiveFormsModule, appRouting, HttpModule, ClipboardModule],
@ -50,6 +52,7 @@ import {AppUserStore} from "./services/stores/app-user.store";
RankService, RankService,
RankStore, RankStore,
AwardingService, AwardingService,
PromotionService,
AppConfig, AppConfig,
Title, Title,
routingProviders, routingProviders,
@ -61,6 +64,7 @@ import {AppUserStore} from "./services/stores/app-user.store";
DecorationComponent, DecorationComponent,
DecorationItemComponent, DecorationItemComponent,
RankItemComponent, RankItemComponent,
FilterRankPipe,
UserItemComponent, UserItemComponent,
SquadItemComponent, SquadItemComponent,
ShowErrorComponent, ShowErrorComponent,

View File

@ -10,6 +10,7 @@ import {armyRoutes, armyRoutingComponents} from "./army/army.routing";
import {SignupComponent} from "./login/signup.component"; import {SignupComponent} from "./login/signup.component";
import {AdminComponent} from "./admin/admin.component"; import {AdminComponent} from "./admin/admin.component";
import {RequestAwardComponent} from "./request/award/req-award.component"; import {RequestAwardComponent} from "./request/award/req-award.component";
import {RequestPromotionComponent} from "./request/promotion/req-promotion.component";
export const appRoutes: Routes = [ export const appRoutes: Routes = [
@ -21,6 +22,7 @@ export const appRoutes: Routes = [
{path: 'signup', component: SignupComponent}, {path: 'signup', component: SignupComponent},
{path: 'request-award', component: RequestAwardComponent, canActivate: [LoginGuardSQL]}, {path: 'request-award', component: RequestAwardComponent, canActivate: [LoginGuardSQL]},
{path: 'request-promotion', component: RequestPromotionComponent, canActivate: [LoginGuardSQL]},
{path: 'cc-users', children: usersRoutes, canActivate: [LoginGuardHL]}, {path: 'cc-users', children: usersRoutes, canActivate: [LoginGuardHL]},
{path: 'cc-squads', children: squadsRoutes, canActivate: [LoginGuardHL]}, {path: 'cc-squads', children: squadsRoutes, canActivate: [LoginGuardHL]},
@ -36,7 +38,7 @@ export const appRoutes: Routes = [
export const appRouting = RouterModule.forRoot(appRoutes); export const appRouting = RouterModule.forRoot(appRoutes);
export const routingComponents = [LoginComponent, SignupComponent, RequestAwardComponent, AdminComponent, ...armyRoutingComponents , NotFoundComponent, ...usersRoutingComponents, export const routingComponents = [LoginComponent, SignupComponent, RequestAwardComponent, RequestPromotionComponent, AdminComponent, ...armyRoutingComponents , NotFoundComponent, ...usersRoutingComponents,
...squadsRoutingComponents, ...decorationsRoutingComponents, ...ranksRoutingComponents]; ...squadsRoutingComponents, ...decorationsRoutingComponents, ...ranksRoutingComponents];
export const routingProviders = [LoginGuardHL]; export const routingProviders = [LoginGuardHL];

View File

@ -0,0 +1,23 @@
import {Pipe, PipeTransform} from "@angular/core";
/**
* Filter Pipe to filter specific rank by level number in template
*
* @author: HardiReady
*/
@Pipe({name: 'rankfilter'})
export class FilterRankPipe implements PipeTransform {
transform(items: any[], filter: any): any {
// filter items array, items which match and return true will be kept, false will be filtered out
let res = items.filter(item => item.level === filter);
if (res.length === 0) {
return [{name: '-'}];
}
else {
return res
}
}
}

View File

@ -40,6 +40,12 @@ export interface Award {
confirmed?: boolean; confirmed?: boolean;
} }
export interface Promotion {
userId?: string
oldRankLvl: number,
newRankLvl: number
}
export interface Decoration { export interface Decoration {
_id?: string; _id?: string;
name?: string; name?: string;

View File

@ -14,7 +14,7 @@
.table-container { .table-container {
margin-top: 40px; margin-top: 40px;
overflow-x: auto; overflow-x: auto;
width: 50%; width: 65%;
} }
/* enable scrolling for long list of awardings */ /* enable scrolling for long list of awardings */

View File

@ -8,9 +8,8 @@
id="user" id="user"
[(ngModel)]="user" [(ngModel)]="user"
[compareWith]="equals" [compareWith]="equals"
required (change)="toggleUser(decoPreview, decoDescription)"
(change)="toggleUser()" required>
style="min-width: 200px;">
<option *ngFor="let user of users" [ngValue]="user"> <option *ngFor="let user of users" [ngValue]="user">
{{user.username}} {{user.username}}
</option> </option>
@ -18,22 +17,18 @@
</div> </div>
<div *ngIf="showForm"> <div class="form-group" *ngIf="showForm">
<div class="form-group">
<label for="decoration">Auszeichnung</label> <label for="decoration">Auszeichnung</label>
<select class="form-control" <select class="form-control"
name="decoration" name="decoration"
id="decoration" id="decoration"
#decorationField [(ngModel)]="decoration"
(change)="toggleDecoPreview(decoDescription, decorationField.value, decoPreview)" (change)="toggleDecoPreview(decoDescription, decoPreview)"
[ngModel]="undefined"
required> required>
<option *ngFor="let deco of decorations" [value]="deco._id"> <option *ngFor="let deco of decorations" [ngValue]="deco">
{{deco.fraction == 'BLUFOR'? 'NATO' : deco.fraction == 'OPFOR'? 'CSAT' : 'Global'}}: {{deco.name}} {{deco.fraction == 'BLUFOR'? 'NATO' : deco.fraction == 'OPFOR'? 'CSAT' : 'Global'}}: {{deco.name}}
</option> </option>
</select> </select>
<show-error text="Auszeichnung" path="decoration"></show-error>
</div> </div>
<div class="div-table-row" [style.display]="decoPreviewDisplay" style="margin-top: 5px; margin-bottom:10px"> <div class="div-table-row" [style.display]="decoPreviewDisplay" style="margin-top: 5px; margin-bottom:10px">
@ -46,11 +41,10 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group" *ngIf="showForm">
<label for="reason">Begründung</label> <label for="reason">Begründung</label>
<textarea class="form-control center-block" name="reason" [ngModel]="undefined" required <textarea class="form-control center-block" name="reason" [(ngModel)]="reason" required
id="reason" placeholder="Begründung eingeben..." rows="3" #awardTextArea></textarea> id="reason" placeholder="Begründung eingeben..." rows="3" ></textarea>
<show-error text="Begründung" path="reason"></show-error>
</div> </div>
<button id="cancel" <button id="cancel"
@ -60,8 +54,9 @@
</button> </button>
<button id="save" <button id="save"
(click)="addAwarding(decorationField, awardTextArea, decoPreview, decoDescription)" (click)="addAwarding(decoPreview, decoDescription)"
class="btn btn-default" class="btn btn-default"
*ngIf="showForm"
[disabled]="!form.valid"> [disabled]="!form.valid">
Bestätigen Bestätigen
</button> </button>
@ -72,7 +67,7 @@
Erfolgreich gespeichert Erfolgreich gespeichert
</span> </span>
<div class="table-container"> <div class="table-container" *ngIf="showForm">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -111,6 +106,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</form> </form>

View File

@ -21,6 +21,10 @@ export class RequestAwardComponent {
user: User = {}; user: User = {};
decoration: Decoration = null;
reason: string = '';
decorations: Decoration[]; decorations: Decoration[];
awards: Award[]; awards: Award[];
@ -39,12 +43,16 @@ export class RequestAwardComponent {
ngOnInit() { ngOnInit() {
let currentUser = JSON.parse(localStorage.getItem('currentUser')); let currentUser = JSON.parse(localStorage.getItem('currentUser'));
// show only current users squad members // show only current users squad members
this.userService.findUsers('', undefined, currentUser.squad).subscribe(users => { this.userService.findUsers('', undefined, currentUser.squad._id).subscribe(users => {
this.users = users; this.users = users;
}); });
} }
toggleUser() { toggleUser(previewImage, decoDescription) {
this.decoration = null;
if (previewImage && decoDescription) {
previewImage.src = decoDescription.innerHTML = '';
}
this.decorationService.findDecorations('', this.user.squad.fraction).subscribe(decorations => { this.decorationService.findDecorations('', this.user.squad.fraction).subscribe(decorations => {
this.decorations = decorations; this.decorations = decorations;
}); });
@ -57,36 +65,33 @@ export class RequestAwardComponent {
} }
toggleDecoPreview(descriptionField, decorationId, image) { toggleDecoPreview(descriptionField, image) {
this.decoPreviewDisplay = 'flex'; // visible & keep same height for all children this.decoPreviewDisplay = 'flex'; // visible & keep same height for all children
const description = this.decorations.find( const description = this.decorations.find(
decoration => decoration._id === decorationId decoration => decoration._id === this.decoration._id
).description; ).description;
image.src = 'resource/decoration/' + decorationId + '.png'; image.src = 'resource/decoration/' + this.decoration._id + '.png';
descriptionField.innerHTML = description; descriptionField.innerHTML = description;
} }
addAwarding(decorationField, reasonField, previewImage, descriptionField) { addAwarding(previewImage, descriptionField) {
const decorationId = decorationField.value; if (this.decoration._id && this.reason.length > 0) {
const reason = reasonField.value;
if (decorationId && reason.length > 0) {
const award = { const award = {
"userId": this.user._id, "userId": this.user._id,
"decorationId": decorationId, "decorationId": this.decoration._id,
"reason": reason, "reason": this.reason,
"date": Date.now() "date": Date.now()
}; };
this.awardingService.requestAwarding(award).subscribe(() => { this.awardingService.requestAwarding(award).subscribe(() => {
this.awardingService.getUserAwardings(this.user._id) this.awardingService.getUserAwardings(this.user._id)
.subscribe(awards => { .subscribe(awards => {
this.awards = awards; this.awards = awards;
console.log(awards[0]) this.decoration = null;
this.reason = previewImage.src = descriptionField.innerHTML = '';
this.decoPreviewDisplay = 'none'; this.decoPreviewDisplay = 'none';
decorationField.value = undefined;
reasonField.value = previewImage.src = descriptionField.innerHTML = '';
this.showSuccessLabel = true; this.showSuccessLabel = true;
setTimeout(() => { setTimeout(() => {
this.showSuccessLabel = false; this.showSuccessLabel = false;

View File

@ -0,0 +1,43 @@
.decoration-preview {
padding: 5px;
}
.trash {
cursor: pointer;
}
.table {
overflow-wrap: break-word;
table-layout: fixed;
}
.table-container {
margin-top: 40px;
overflow-x: auto;
width: 65%;
}
/* enable scrolling for long list of awardings */
.overview {
position: fixed;
overflow-y: scroll;
overflow-x: hidden;
width: 100%;
border-left: thin solid lightgrey;
padding: 20px 0 0 50px;
margin-left: 10px;
height: 100vh;
bottom: 10px;
}
.form-group {
width: 25%;
}
h3 {
margin: 80px 0 20px -20px;
}
label {
display: block;
}

View File

@ -0,0 +1,105 @@
<form #form="ngForm" class="overview">
<h3>Beförderung beantragen</h3>
<div class="form-group">
<label for="user">Teilnehmer</label>
<select class="form-control"
name="user"
id="user"
[(ngModel)]="user"
[compareWith]="equals"
required
(change)="toggleUser()">
<option *ngFor="let user of users" [ngValue]="user">
{{user.username}}
</option>
</select>
</div>
<div *ngIf="showForm">
<div class="form-group">
<label for="user">Aktueller Rang</label>
<input class="form-control"
[(ngModel)]="user.rank.name"
[ngModelOptions]="{standalone: true}"
readonly>
</div>
<div class="form-group">
<label for="decoration">Neuer Rang</label>
<select class="form-control"
name="decoration"
id="decoration"
#decorationField
[(ngModel)]="newLevel"
required>
<option *ngFor="let rank of ranks" [ngValue]="rank.level">
{{rank.name}}
</option>
</select>
</div>
</div>
<button id="cancel"
(click)="cancel()"
class="btn btn-default">
Abbrechen
</button>
<button id="save"
*ngIf="showForm"
(click)="addPromotion()"
class="btn btn-default"
[disabled]="newLevel === user.rank.level">
Bestätigen
</button>
<span *ngIf="showSuccessLabel"
class="label label-success label-small"
style="margin-left: inherit">
Erfolgreich gespeichert
</span>
<div class="table-container">
<label>Beförderungsanträge</label>
<table class="table table-hover">
<thead>
<tr>
<th class="col-sm-1">Teilnehmer</th>
<th class="col-sm-1">Alter Rang</th>
<th class="col-sm-1">Neuer Rang</th>
<th class="col-sm-1 ">Antragsteller</th>
<th class="col-sm-1 text-center">Datum</th>
<th class="col-sm-1 text-center">Status</th>
</tr>
</thead>
<tbody *ngFor="let promotion of uncheckedPromotions">
<tr>
<td class="table-cell-id">
{{promotion.userId.username}}
</td>
<td *ngFor="let rank of (ranks | rankfilter: promotion.oldRankLvl)">
{{rank.name}}
</td>
<td *ngFor="let rank of (ranks | rankfilter: promotion.newRankLvl)">
{{rank.name}}
</td>
<td>
{{promotion.proposer.username}}
</td>
<td class="text-center">
{{promotion.timestamp | date: 'dd.MM.yyyy'}}
</td>
<td class="text-center">
{{promotion.confirmed === 0? 'In Bearbeitung' : (promotion.confirmed === 1? 'Genehmigt' : 'Abgelehnt')}}
</td>
</tr>
</tbody>
</table>
</div>
</form>

View File

@ -0,0 +1,96 @@
import {Component, ViewChild} from "@angular/core";
import {ActivatedRoute, Router} from "@angular/router";
import {Rank, User} from "../../models/model-interfaces";
import {NgForm} from "@angular/forms";
import {UserService} from "../../services/user-service/user.service";
import {RankService} from "../../services/rank-service/rank.service";
import {PromotionService} from "../../services/promotion-service/promotion.service";
@Component({
templateUrl: './req-promotion.component.html',
styleUrls: ['./req-promotion.component.css'],
})
export class RequestPromotionComponent {
@ViewChild(NgForm) form: NgForm;
showForm = false;
showSuccessLabel = false;
user: User = {};
newLevel: number;
ranks: Rank[];
users: User[];
uncheckedPromotions = [];
constructor(private router: Router,
private route: ActivatedRoute,
private userService: UserService,
private rankService: RankService,
private promotionService: PromotionService) {
}
ngOnInit() {
let currentUser = JSON.parse(localStorage.getItem('currentUser'));
// show only current users squad members
this.userService.findUsers('', undefined, currentUser.squad._id).subscribe(users => {
this.users = users;
});
this.rankService.findRanks('', currentUser.squad.fraction).subscribe(ranks => {
this.ranks = ranks;
});
this.promotionService.getSquadPromotions(currentUser.squad._id).subscribe(promotions => {
this.uncheckedPromotions = promotions;
})
}
toggleUser() {
this.showForm = true;
this.newLevel = this.user.rank.level;
}
addPromotion() {
const promotion = {
"userId": this.user._id,
"oldRankLvl": this.user.rank.level,
"newRankLvl": this.newLevel,
};
this.promotionService.requestPromotion(promotion).subscribe();
this.showSuccessLabel = true;
setTimeout(() => {
this.showSuccessLabel = false;
}, 2000);
this.showForm = false;
this.user = {};
let currentUser = JSON.parse(localStorage.getItem('currentUser'));
this.promotionService.getSquadPromotions(currentUser.squad._id).subscribe(promotions => {
this.uncheckedPromotions = promotions;
})
}
cancel() {
this.router.navigate(['..'], {relativeTo: this.route});
return false;
}
/**
* compare ngValue with ngModel to assign selected element
*/
equals(o1: User, o2: User) {
if (o1 && o2) {
return o1._id === o2._id;
}
}
}

View File

@ -8,9 +8,6 @@ import {HttpClient} from "../http-client";
@Injectable() @Injectable()
export class AwardingService { export class AwardingService {
users$: Observable<User[]>;
constructor(private http: HttpClient, constructor(private http: HttpClient,
private config: AppConfig) { private config: AppConfig) {
} }

View File

@ -0,0 +1,27 @@
import {Injectable} from "@angular/core";
import {AppConfig} from "../../app.config";
import {HttpClient} from "../http-client";
import {Promotion} from "../../models/model-interfaces";
@Injectable()
export class PromotionService {
constructor(private http: HttpClient,
private config: AppConfig) {
}
getSquadPromotions(squadId: string) {
return this.http.get(this.config.apiUrl + this.config.apiPromotionPath + '?squadId=' + squadId)
.map(res => res.json())
}
requestPromotion(promotion: Promotion) {
return this.http.post(this.config.apiUrl + this.config.apiPromotionPath, promotion)
}
deletePromotion(promotionId) {
return this.http.delete(this.config.apiUrl + this.config.apiPromotionPath + promotionId)
}
}