diff --git a/api/config/api-url.js b/api/config/api-url.js index 58b1170..d7dbc34 100644 --- a/api/config/api-url.js +++ b/api/config/api-url.js @@ -10,5 +10,6 @@ module.exports = { signatures: '/signatures', squads: '/squads', users: '/users', - account: '/account' + account: '/account', + request: '/request' }; diff --git a/api/models/awarding.js b/api/models/awarding.js index fa2b2f4..d653369 100644 --- a/api/models/awarding.js +++ b/api/models/awarding.js @@ -16,6 +16,11 @@ const AwardingSchema = new Schema({ type: String, required: true }, + proposer: { + type: mongoose.Schema.Types.ObjectId, + ref: 'AppUser', + required: true + }, confirmed: { type: Boolean, default: true diff --git a/api/models/promotion.js b/api/models/promotion.js new file mode 100644 index 0000000..acd3706 --- /dev/null +++ b/api/models/promotion.js @@ -0,0 +1,33 @@ +"use strict"; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const PromotionSchema = new Schema({ + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + proposer: { + type: mongoose.Schema.Types.ObjectId, + ref: 'AppUser', + required: true + }, + rankLvl: { + type: Number, + get: v => Math.round(v), + set: v => Math.round(v), + required: true + }, + confirmed: { + type: Boolean, + default: true + } +}, { + collection: 'promotion', + timestamps: {createdAt: 'timestamp'} +}); +// optional more indices +PromotionSchema.index({timestamp: 1}); + +module.exports = mongoose.model('Promotion', PromotionSchema); diff --git a/api/routes/authenticate.js b/api/routes/authenticate.js index 8ab87fa..955a0c4 100644 --- a/api/routes/authenticate.js +++ b/api/routes/authenticate.js @@ -61,6 +61,7 @@ let authCheck = (username, password, res) => { _id: user._id, username: user.username, permission: user.permission, + squad: user.squad, token: jwt.sign({sub: user._id}, config.secret, {expiresIn: diff * 60}), tokenExpireDate: new Date(Date.now().valueOf() + diff * 60000 - 1000) }); diff --git a/api/routes/awardings.js b/api/routes/awardings.js index d05b957..96beea3 100644 --- a/api/routes/awardings.js +++ b/api/routes/awardings.js @@ -9,9 +9,23 @@ const codes = require('./http-codes'); const routerHandling = require('../middleware/router-handling'); +const apiAuthenticationMiddleware = require('../middleware/auth-middleware'); +const checkHl = require('../middleware/permission-check').checkHl; + // Mongoose Model using mongoDB const AwardingModel = require('../models/awarding'); +// result set for proposer(appUser) population +const resultSet = { + '__v': 0, + 'updatedAt': 0, + 'timestamp': 0, + 'password': 0, + 'permission': 0, + 'secret': 0, + 'activated': 0 +}; + const awarding = express.Router(); @@ -39,7 +53,7 @@ awarding.route('/') }); } else { AwardingModel.find(filter, {}, {sort: {date: 'desc'}}) - .populate('decorationId').exec((err, items) => { + .populate('decorationId').populate('proposer', resultSet).exec((err, items) => { if (err) { err.status = codes.servererror; return next(err); @@ -56,17 +70,18 @@ awarding.route('/') } }) - .post((req, res, next) => { - const rank = new AwardingModel(req.body); + .post(apiAuthenticationMiddleware, checkHl, (req, res, next) => { + const award = new AwardingModel(req.body); + award.proposer = req.user._id; // timestamp and default are set automatically by Mongoose Schema Validation - rank.save((err) => { + award.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 = rank; + res.locals.items = award; next(); }); }) @@ -76,7 +91,7 @@ awarding.route('/') ); awarding.route('/:id') - .delete((req, res, next) => { + .delete(apiAuthenticationMiddleware, checkHl, (req, res, next) => { AwardingModel.findByIdAndRemove(req.params.id, (err, item) => { if (err) { err.status = codes.wrongrequest; diff --git a/api/routes/request.js b/api/routes/request.js new file mode 100644 index 0000000..f2e09c3 --- /dev/null +++ b/api/routes/request.js @@ -0,0 +1,54 @@ +"use strict"; + +// modules +const express = require('express'); +const logger = require('debug')('cc:awardings'); + +// HTTP status codes by name +const codes = require('./http-codes'); + +const routerHandling = require('../middleware/router-handling'); + +// Mongoose Model using mongoDB +const AwardingModel = require('../models/awarding'); + +const request = express.Router(); + + +// routes ********************** +request.route('/award') + + .post((req, res, next) => { + const award = new AwardingModel(req.body); + award.confirmed = false; + award.proposer = req.user._id; + // timestamp and default are set automatically by Mongoose Schema Validation + award.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 = award; + next(); + }); + }) + + .all( + routerHandling.httpMethodNotAllowed + ); + +request.route('/promotion') + + + .all( + 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 +request.use(routerHandling.emptyResponse); + + +module.exports = request; diff --git a/api/routes/users.js b/api/routes/users.js index 797cf72..f8942ef 100644 --- a/api/routes/users.js +++ b/api/routes/users.js @@ -42,8 +42,9 @@ users.route('/') else { const nameQuery = req.query.q; const fractionFilter = req.query.fractFilter; + const squadFilter = req.query.squadId; - UserModel.find({}, res.locals.filter, res.locals.limitskip, (err, users) => { + UserModel.find({}, (err, users) => { if (err) return next(err); if (users.length === 0) { res.locals.items = users; @@ -56,8 +57,18 @@ users.route('/') // filter by name if (!nameQuery || (nameQuery && user.username.toLowerCase().includes(nameQuery.toLowerCase()))) { getExtendedUser(user, next, (extUser) => { + + // filter by squad + if (squadFilter) { + if (extUser.squad && extUser.squad._id.toString() === squadFilter) { + resUsers.push(extUser); + } + else { + rowsLength -= 1; + } + } // filter by fraction - if (!fractionFilter || + else if (!fractionFilter || (fractionFilter && extUser.squad && extUser.squad.fraction.toLowerCase() === fractionFilter) || (fractionFilter && fractionFilter === 'unassigned' && !extUser.squad)) { resUsers.push(extUser); diff --git a/api/server.js b/api/server.js index a5fa160..51a3b16 100644 --- a/api/server.js +++ b/api/server.js @@ -15,7 +15,7 @@ const urls = require('./config/api-url'); const restAPIchecks = require('./middleware/request-checks.js'); const errorResponseWare = require('./middleware/error-response'); const apiAuthenticationMiddleware = require('./middleware/auth-middleware'); -const checkHl = require('./middleware/permission-check').checkHl; +const checkSql = require('./middleware/permission-check').checkSql; const checkAdmin = require('./middleware/permission-check').checkAdmin; const signatureCronJob = require('./cron-job/update-signatures'); @@ -28,6 +28,7 @@ const squadRouter = require('./routes/squads'); const rankRouter = require('./routes/ranks'); const decorationRouter = require('./routes/decorations'); const awardingRouter = require('./routes/awardings'); +const requestRouter = require('./routes/request'); const signatureRouter = require('./routes/signatures'); const commandRouter = require('./routes/command'); @@ -72,7 +73,8 @@ app.use(urls.users, userRouter); app.use(urls.squads, squadRouter); app.use(urls.ranks, rankRouter); app.use(urls.decorations, decorationRouter); -app.use(urls.awards, apiAuthenticationMiddleware, checkHl, awardingRouter); +app.use(urls.request, apiAuthenticationMiddleware, checkSql, requestRouter); +app.use(urls.awards, awardingRouter); app.use(urls.command, apiAuthenticationMiddleware, checkAdmin, commandRouter); app.use(urls.account, apiAuthenticationMiddleware, checkAdmin, accountRouter); diff --git a/static/src/app/app.config.ts b/static/src/app/app.config.ts index b9c0608..0869196 100644 --- a/static/src/app/app.config.ts +++ b/static/src/app/app.config.ts @@ -12,5 +12,6 @@ export class AppConfig { public readonly apiSquadPath = '/squads/'; public readonly apiUserPath = '/users/'; public readonly apiOverviewPath = '/overview'; + public readonly apiRequestAwardPath = '/request/award' } diff --git a/static/src/app/app.routing.ts b/static/src/app/app.routing.ts index 709810d..abafadb 100644 --- a/static/src/app/app.routing.ts +++ b/static/src/app/app.routing.ts @@ -1,7 +1,7 @@ import {Routes, RouterModule} from '@angular/router'; import {LoginComponent} from './login/index'; import {NotFoundComponent} from './not-found/not-found.component'; -import {LoginGuardAdmin, LoginGuardHL} from './login/login.guard'; +import {LoginGuardAdmin, LoginGuardHL, LoginGuardSQL} from './login/login.guard'; import {usersRoutes, usersRoutingComponents} from "./users/users.routing"; import {squadsRoutes, squadsRoutingComponents} from "./squads/squads.routing"; import {decorationsRoutes, decorationsRoutingComponents} from "./decorations/decoration.routing"; @@ -9,6 +9,7 @@ import {ranksRoutes, ranksRoutingComponents} from "./ranks/ranks.routing"; 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"; export const appRoutes: Routes = [ @@ -18,6 +19,9 @@ export const appRoutes: Routes = [ {path: 'login', component: LoginComponent}, {path: 'signup', component: SignupComponent}, + + {path: 'request-award', component: RequestAwardComponent, canActivate: [LoginGuardSQL]}, + {path: 'cc-users', children: usersRoutes, canActivate: [LoginGuardHL]}, {path: 'cc-squads', children: squadsRoutes, canActivate: [LoginGuardHL]}, {path: 'cc-decorations', children: decorationsRoutes, canActivate: [LoginGuardHL]}, @@ -32,7 +36,7 @@ export const appRoutes: Routes = [ export const appRouting = RouterModule.forRoot(appRoutes); -export const routingComponents = [LoginComponent, SignupComponent, AdminComponent, ...armyRoutingComponents , NotFoundComponent, ...usersRoutingComponents, +export const routingComponents = [LoginComponent, SignupComponent, RequestAwardComponent, AdminComponent, ...armyRoutingComponents , NotFoundComponent, ...usersRoutingComponents, ...squadsRoutingComponents, ...decorationsRoutingComponents, ...ranksRoutingComponents]; export const routingProviders = [LoginGuardHL]; diff --git a/static/src/app/army/army-member.component.html b/static/src/app/army/army-member.component.html index 01ebaed..1392d10 100644 --- a/static/src/app/army/army-member.component.html +++ b/static/src/app/army/army-member.component.html @@ -32,7 +32,7 @@ - + diff --git a/static/src/app/models/model-interfaces.ts b/static/src/app/models/model-interfaces.ts index c992685..c0f82f2 100644 --- a/static/src/app/models/model-interfaces.ts +++ b/static/src/app/models/model-interfaces.ts @@ -35,6 +35,7 @@ export interface Award { userId: string, decorationId?: Decoration; reason?: string; + proposer?: AppUser; date?: number; // since Date.now() returns a number confirmed?: boolean; } diff --git a/static/src/app/request/award/req-award.component.css b/static/src/app/request/award/req-award.component.css new file mode 100644 index 0000000..7e4bb1e --- /dev/null +++ b/static/src/app/request/award/req-award.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: 50%; +} + +/* 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/award/req-award.component.html b/static/src/app/request/award/req-award.component.html new file mode 100644 index 0000000..0bac083 --- /dev/null +++ b/static/src/app/request/award/req-award.component.html @@ -0,0 +1,116 @@ +
+

Auszeichnung beantragen

+ +
+ + +
+ + +
+
+ + + + +
+ +
+
+ +
+
+   +
+
+ +
+ + + +
+ + + + + + + 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 new file mode 100644 index 0000000..1446d44 --- /dev/null +++ b/static/src/app/request/award/req-award.component.ts @@ -0,0 +1,113 @@ +import {Component, ViewChild} from "@angular/core"; +import {ActivatedRoute, Router} from "@angular/router"; +import {Award, Decoration, User} from "../../models/model-interfaces"; +import {NgForm} from "@angular/forms"; +import {AwardingService} from "../../services/awarding-service/awarding.service"; +import {DecorationService} from "../../services/decoration-service/decoration.service"; +import {UserService} from "../../services/user-service/user.service"; + + +@Component({ + templateUrl: './req-award.component.html', + styleUrls: ['./req-award.component.css'], +}) +export class RequestAwardComponent { + + @ViewChild(NgForm) form: NgForm; + + showForm = false; + + showSuccessLabel = false; + + user: User = {}; + + decorations: Decoration[]; + + awards: Award[]; + + users: User[]; + + decoPreviewDisplay = 'none'; + + constructor(private router: Router, + private route: ActivatedRoute, + private userService: UserService, + private awardingService: AwardingService, + private decorationService: DecorationService) { + } + + ngOnInit() { + let currentUser = JSON.parse(localStorage.getItem('currentUser')); + // show only current users squad members + this.userService.findUsers('', undefined, currentUser.squad).subscribe(users => { + this.users = users; + }); + } + + toggleUser() { + this.decorationService.findDecorations('', this.user.squad.fraction).subscribe(decorations => { + this.decorations = decorations; + }); + + this.awardingService.getUserAwardings(this.user._id).subscribe(awards => { + this.awards = awards; + }); + + this.showForm = true; + } + + + toggleDecoPreview(descriptionField, decorationId, image) { + this.decoPreviewDisplay = 'flex'; // visible & keep same height for all children + + const description = this.decorations.find( + decoration => decoration._id === decorationId + ).description; + + image.src = 'resource/decoration/' + decorationId + '.png'; + descriptionField.innerHTML = description; + + } + + addAwarding(decorationField, reasonField, previewImage, descriptionField) { + const decorationId = decorationField.value; + const reason = reasonField.value; + if (decorationId && reason.length > 0) { + const award = { + "userId": this.user._id, + "decorationId": decorationId, + "reason": 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.decoPreviewDisplay = 'none'; + decorationField.value = undefined; + reasonField.value = previewImage.src = descriptionField.innerHTML = ''; + this.showSuccessLabel = true; + setTimeout(() => { + this.showSuccessLabel = false; + }, 2000) + }) + }) + } + } + + 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 6695d29..60c1444 100644 --- a/static/src/app/services/awarding-service/awarding.service.ts +++ b/static/src/app/services/awarding-service/awarding.service.ts @@ -27,6 +27,10 @@ export class AwardingService { return this.http.post(this.config.apiUrl + this.config.apiAwardPath, award) } + requestAwarding(award) { + return this.http.post(this.config.apiUrl + this.config.apiRequestAwardPath, award) + } + deleteAwarding(awardingId) { return this.http.delete(this.config.apiUrl + this.config.apiAwardPath + awardingId) } diff --git a/static/src/app/services/user-service/user.service.ts b/static/src/app/services/user-service/user.service.ts index cb212f0..d36e9ce 100644 --- a/static/src/app/services/user-service/user.service.ts +++ b/static/src/app/services/user-service/user.service.ts @@ -17,12 +17,15 @@ export class UserService { this.users$ = userStore.items$; } - findUsers(query = '', fractionFilter?) { + findUsers(query = '', fractionFilter?, squadFilter?) { const searchParams = new URLSearchParams(); searchParams.append('q', query); if (fractionFilter) { searchParams.append('fractFilter', fractionFilter); } + if (squadFilter) { + searchParams.append('squadId', squadFilter); + } this.http.get(this.config.apiUrl + this.config.apiUserPath, searchParams) .map(res => res.json()) .do((users) => { diff --git a/static/src/app/users/award-user/award-user.component.html b/static/src/app/users/award-user/award-user.component.html index fc714b4..416b614 100644 --- a/static/src/app/users/award-user/award-user.component.html +++ b/static/src/app/users/award-user/award-user.component.html @@ -70,6 +70,7 @@ Bezeichnung Begründung Datum + Status @@ -90,6 +91,9 @@ {{award.date | date: 'dd.MM.yyyy'}} + + {{award.confirmed? 'Bestätigt' : 'In Bearbeitung'}} +