diff --git a/api/models/promotion.js b/api/models/promotion.js index acd3706..c809bd7 100644 --- a/api/models/promotion.js +++ b/api/models/promotion.js @@ -13,15 +13,25 @@ const PromotionSchema = new Schema({ ref: 'AppUser', required: true }, - rankLvl: { + oldRankLvl: { + type: Number, + get: v => Math.round(v), + set: v => Math.round(v), + required: true + }, + newRankLvl: { type: Number, get: v => Math.round(v), set: v => Math.round(v), required: true }, confirmed: { - type: Boolean, - default: true + type: Number, + get: v => Math.round(v), + set: v => Math.round(v), + min: 0, + max: 2, + required: true } }, { collection: 'promotion', diff --git a/api/routes/authenticate.js b/api/routes/authenticate.js index 955a0c4..1dfb8aa 100644 --- a/api/routes/authenticate.js +++ b/api/routes/authenticate.js @@ -47,7 +47,7 @@ authenticate.route('/') let authCheck = (username, password, res) => { 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); const diff = 3 * 60 * 24; // time till expiration [minutes] diff --git a/api/routes/request.js b/api/routes/request.js index f2e09c3..e107734 100644 --- a/api/routes/request.js +++ b/api/routes/request.js @@ -10,7 +10,20 @@ const codes = require('./http-codes'); const routerHandling = require('../middleware/router-handling'); // Mongoose Model using mongoDB +const UserModel = require('../models/user'); 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(); @@ -41,6 +54,51 @@ request.route('/award') 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( routerHandling.httpMethodNotAllowed diff --git a/static/src/app/app.config.ts b/static/src/app/app.config.ts index 0869196..4b41471 100644 --- a/static/src/app/app.config.ts +++ b/static/src/app/app.config.ts @@ -12,6 +12,7 @@ export class AppConfig { public readonly apiSquadPath = '/squads/'; public readonly apiUserPath = '/users/'; public readonly apiOverviewPath = '/overview'; - public readonly apiRequestAwardPath = '/request/award' + public readonly apiRequestAwardPath = '/request/award'; + public readonly apiPromotionPath = '/request/promotion'; } diff --git a/static/src/app/app.module.ts b/static/src/app/app.module.ts index ffd2f67..9db2858 100644 --- a/static/src/app/app.module.ts +++ b/static/src/app/app.module.ts @@ -29,6 +29,8 @@ import {ArmyService} from "./services/army-service/army.service"; import { ClipboardModule } from 'ngx-clipboard'; import {AppUserService} from "./services/app-user-service/app-user.service"; import {AppUserStore} from "./services/stores/app-user.store"; +import {PromotionService} from "./services/promotion-service/promotion.service"; +import {FilterRankPipe} from "./filter/filter.pipe"; @NgModule({ imports: [BrowserModule, FormsModule, ReactiveFormsModule, appRouting, HttpModule, ClipboardModule], @@ -50,6 +52,7 @@ import {AppUserStore} from "./services/stores/app-user.store"; RankService, RankStore, AwardingService, + PromotionService, AppConfig, Title, routingProviders, @@ -61,6 +64,7 @@ import {AppUserStore} from "./services/stores/app-user.store"; DecorationComponent, DecorationItemComponent, RankItemComponent, + FilterRankPipe, UserItemComponent, SquadItemComponent, ShowErrorComponent, diff --git a/static/src/app/app.routing.ts b/static/src/app/app.routing.ts index abafadb..462f7a2 100644 --- a/static/src/app/app.routing.ts +++ b/static/src/app/app.routing.ts @@ -10,6 +10,7 @@ import {armyRoutes, armyRoutingComponents} from "./army/army.routing"; import {SignupComponent} from "./login/signup.component"; import {AdminComponent} from "./admin/admin.component"; import {RequestAwardComponent} from "./request/award/req-award.component"; +import {RequestPromotionComponent} from "./request/promotion/req-promotion.component"; export const appRoutes: Routes = [ @@ -21,6 +22,7 @@ export const appRoutes: Routes = [ {path: 'signup', component: SignupComponent}, {path: 'request-award', component: RequestAwardComponent, canActivate: [LoginGuardSQL]}, + {path: 'request-promotion', component: RequestPromotionComponent, canActivate: [LoginGuardSQL]}, {path: 'cc-users', children: usersRoutes, 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 routingComponents = [LoginComponent, SignupComponent, RequestAwardComponent, AdminComponent, ...armyRoutingComponents , NotFoundComponent, ...usersRoutingComponents, +export const routingComponents = [LoginComponent, SignupComponent, RequestAwardComponent, RequestPromotionComponent, AdminComponent, ...armyRoutingComponents , NotFoundComponent, ...usersRoutingComponents, ...squadsRoutingComponents, ...decorationsRoutingComponents, ...ranksRoutingComponents]; export const routingProviders = [LoginGuardHL]; diff --git a/static/src/app/filter/filter.pipe.ts b/static/src/app/filter/filter.pipe.ts new file mode 100644 index 0000000..1df80f1 --- /dev/null +++ b/static/src/app/filter/filter.pipe.ts @@ -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 + } + } + +} diff --git a/static/src/app/models/model-interfaces.ts b/static/src/app/models/model-interfaces.ts index c0f82f2..d62821b 100644 --- a/static/src/app/models/model-interfaces.ts +++ b/static/src/app/models/model-interfaces.ts @@ -40,6 +40,12 @@ export interface Award { confirmed?: boolean; } +export interface Promotion { + userId?: string + oldRankLvl: number, + newRankLvl: number +} + export interface Decoration { _id?: string; name?: string; diff --git a/static/src/app/request/award/req-award.component.css b/static/src/app/request/award/req-award.component.css index 7e4bb1e..d9356a4 100644 --- a/static/src/app/request/award/req-award.component.css +++ b/static/src/app/request/award/req-award.component.css @@ -14,7 +14,7 @@ .table-container { margin-top: 40px; overflow-x: auto; - width: 50%; + width: 65%; } /* enable scrolling for long list of awardings */ diff --git a/static/src/app/request/award/req-award.component.html b/static/src/app/request/award/req-award.component.html index 0bac083..2304f51 100644 --- a/static/src/app/request/award/req-award.component.html +++ b/static/src/app/request/award/req-award.component.html @@ -8,9 +8,8 @@ id="user" [(ngModel)]="user" [compareWith]="equals" - required - (change)="toggleUser()" - style="min-width: 200px;"> + (change)="toggleUser(decoPreview, decoDescription)" + required> @@ -18,99 +17,94 @@ -
-
- - +
+ + +
- +
+
+
- -
-
- -
-
-   -
-
- -
- - - -
- - - - - - - Erfolgreich gespeichert - - -
- - - - - - - - - - - - - - - - - - - - - - -
BildBezeichnungBegründungAntragstellerDatumStatus
- - - - - {{award.decorationId.name}} - - {{award.reason}} - - {{award.proposer?.username}} - - {{award.date | date: 'dd.MM.yyyy'}} - - {{award.confirmed? 'Bestätigt' : 'In Bearbeitung'}} -
+
+  
+
+ + +
+ + + + + + + Erfolgreich gespeichert + + +
+ + + + + + + + + + + + + + + + + + + + + + +
BildBezeichnungBegründungAntragstellerDatumStatus
+ + + + + {{award.decorationId.name}} + + {{award.reason}} + + {{award.proposer?.username}} + + {{award.date | date: 'dd.MM.yyyy'}} + + {{award.confirmed? 'Bestätigt' : 'In Bearbeitung'}} +
+
+ diff --git a/static/src/app/request/award/req-award.component.ts b/static/src/app/request/award/req-award.component.ts index 1446d44..3d213fe 100644 --- a/static/src/app/request/award/req-award.component.ts +++ b/static/src/app/request/award/req-award.component.ts @@ -21,6 +21,10 @@ export class RequestAwardComponent { user: User = {}; + decoration: Decoration = null; + + reason: string = ''; + decorations: Decoration[]; awards: Award[]; @@ -39,12 +43,16 @@ export class RequestAwardComponent { ngOnInit() { let currentUser = JSON.parse(localStorage.getItem('currentUser')); // 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; }); } - toggleUser() { + toggleUser(previewImage, decoDescription) { + this.decoration = null; + if (previewImage && decoDescription) { + previewImage.src = decoDescription.innerHTML = ''; + } this.decorationService.findDecorations('', this.user.squad.fraction).subscribe(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 const description = this.decorations.find( - decoration => decoration._id === decorationId + decoration => decoration._id === this.decoration._id ).description; - image.src = 'resource/decoration/' + decorationId + '.png'; + image.src = 'resource/decoration/' + this.decoration._id + '.png'; descriptionField.innerHTML = description; } - addAwarding(decorationField, reasonField, previewImage, descriptionField) { - const decorationId = decorationField.value; - const reason = reasonField.value; - if (decorationId && reason.length > 0) { + addAwarding(previewImage, descriptionField) { + if (this.decoration._id && this.reason.length > 0) { const award = { "userId": this.user._id, - "decorationId": decorationId, - "reason": reason, + "decorationId": this.decoration._id, + "reason": this.reason, "date": Date.now() }; this.awardingService.requestAwarding(award).subscribe(() => { this.awardingService.getUserAwardings(this.user._id) .subscribe(awards => { this.awards = awards; - console.log(awards[0]) + this.decoration = null; + this.reason = previewImage.src = descriptionField.innerHTML = ''; this.decoPreviewDisplay = 'none'; - decorationField.value = undefined; - reasonField.value = previewImage.src = descriptionField.innerHTML = ''; this.showSuccessLabel = true; setTimeout(() => { this.showSuccessLabel = false; diff --git a/static/src/app/request/promotion/req-promotion.component.css b/static/src/app/request/promotion/req-promotion.component.css new file mode 100644 index 0000000..d9356a4 --- /dev/null +++ b/static/src/app/request/promotion/req-promotion.component.css @@ -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; +} diff --git a/static/src/app/request/promotion/req-promotion.component.html b/static/src/app/request/promotion/req-promotion.component.html new file mode 100644 index 0000000..fddaa11 --- /dev/null +++ b/static/src/app/request/promotion/req-promotion.component.html @@ -0,0 +1,105 @@ +
+

Beförderung beantragen

+ +
+ + +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + + + + + + Erfolgreich gespeichert + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
TeilnehmerAlter RangNeuer RangAntragstellerDatumStatus
+ {{promotion.userId.username}} + + {{rank.name}} + + {{rank.name}} + + {{promotion.proposer.username}} + + {{promotion.timestamp | date: 'dd.MM.yyyy'}} + + {{promotion.confirmed === 0? 'In Bearbeitung' : (promotion.confirmed === 1? 'Genehmigt' : 'Abgelehnt')}} +
+
+ + +
diff --git a/static/src/app/request/promotion/req-promotion.component.ts b/static/src/app/request/promotion/req-promotion.component.ts new file mode 100644 index 0000000..528a1aa --- /dev/null +++ b/static/src/app/request/promotion/req-promotion.component.ts @@ -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; + } + } + +} diff --git a/static/src/app/services/awarding-service/awarding.service.ts b/static/src/app/services/awarding-service/awarding.service.ts index 60c1444..ece287e 100644 --- a/static/src/app/services/awarding-service/awarding.service.ts +++ b/static/src/app/services/awarding-service/awarding.service.ts @@ -8,9 +8,6 @@ import {HttpClient} from "../http-client"; @Injectable() export class AwardingService { - users$: Observable; - - constructor(private http: HttpClient, private config: AppConfig) { } diff --git a/static/src/app/services/promotion-service/promotion.service.ts b/static/src/app/services/promotion-service/promotion.service.ts new file mode 100644 index 0000000..195cf50 --- /dev/null +++ b/static/src/app/services/promotion-service/promotion.service.ts @@ -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) + } + +} +