Compare commits

..

2 Commits

Author SHA1 Message Date
Florian Hartwich a354b1ddc8 Optimize sign up ux; Add confirmation for promotion 2017-06-11 13:18:03 +02:00
Florian Hartwich 720df45cfd Add hl confirmation menus 2017-06-10 22:07:32 +02:00
26 changed files with 530 additions and 49 deletions

View File

@ -5,10 +5,7 @@ const async = require('async');
const UserModel = require('../models/user'); const UserModel = require('../models/user');
const signatureTool = require('../signature-tool/signature-tool'); const signatureTool = require('../signature-tool/signature-tool');
const createAllSignatures = () => {
// Execute daily @ 02:30 AM
const cronJob = cron.job('00 30 02 * * *', () => {
console.log('\x1b[35m%s\x1b[0m', new Date().toLocaleString() console.log('\x1b[35m%s\x1b[0m', new Date().toLocaleString()
+ ': cron job started - UPDATE SIGNATURES'); + ': cron job started - UPDATE SIGNATURES');
@ -42,6 +39,12 @@ const cronJob = cron.job('00 30 02 * * *', () => {
}) })
}); });
}); };
module.exports = cronJob; // Execute daily @ 02:30 AM
const cronJob = cron.job('00 30 02 * * *', createAllSignatures);
module.exports = {
cronJob: cronJob,
createAllSignatures: createAllSignatures
};

View File

@ -22,8 +22,12 @@ const AwardingSchema = new Schema({
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,
default: 0
}, },
date: { date: {
type: Date, type: Date,

View File

@ -102,7 +102,7 @@ let create = (userParam) => {
if (user) { if (user) {
// username already exists // username already exists
deferred.reject('Username "' + userParam.username + '" is already taken'); deferred.reject(new Error("Username already exists"));
} else { } else {
createUser(); createUser();
} }

View File

@ -36,6 +36,9 @@ awarding.route('/')
if (req.query.userId) { if (req.query.userId) {
filter.userId = req.query.userId; filter.userId = req.query.userId;
} }
if (req.query.inProgress) {
filter.confirmed = 0;
}
if (req.query.simple) { if (req.query.simple) {
AwardingModel.find(filter, {}, {sort: {date: 'desc'}}, (err, items) => { AwardingModel.find(filter, {}, {sort: {date: 'desc'}}, (err, items) => {
if (err) { if (err) {
@ -53,17 +56,26 @@ awarding.route('/')
}); });
} else { } else {
AwardingModel.find(filter, {}, {sort: {date: 'desc'}}) AwardingModel.find(filter, {}, {sort: {date: 'desc'}})
.populate('decorationId').populate('proposer', resultSet).exec((err, items) => { .populate('decorationId').populate('proposer', resultSet).populate('userId').exec((err, items) => {
if (err) { if (err) {
err.status = codes.servererror; err.status = codes.servererror;
return next(err); return next(err);
// with return before (or after) the next(err) we prevent that the code continues here after next(err) has finished. // with return before (or after) the next(err) we prevent that the code continues here after next(err) has finished.
// this saves an extra else {..} // this saves an extra else {..}
} }
// if the collection is empty we do not send empty arrays back. let results = [];
if (items && items.length > 0) { if (req.query.fractFilter) {
for (let item of items) {
if (item.decorationId.fraction === req.query.fractFilter) {
results.push(item)
}
}
res.locals.items = results;
} else {
res.locals.items = items; res.locals.items = items;
} }
res.locals.processed = true; res.locals.processed = true;
next(); next();
}); });
@ -72,6 +84,7 @@ awarding.route('/')
.post(apiAuthenticationMiddleware, checkHl, (req, res, next) => { .post(apiAuthenticationMiddleware, checkHl, (req, res, next) => {
const award = new AwardingModel(req.body); const award = new AwardingModel(req.body);
award.confirmed = 1;
award.proposer = req.user._id; award.proposer = req.user._id;
// timestamp and default are set automatically by Mongoose Schema Validation // timestamp and default are set automatically by Mongoose Schema Validation
award.save((err) => { award.save((err) => {
@ -91,6 +104,36 @@ awarding.route('/')
); );
awarding.route('/:id') awarding.route('/:id')
.patch(apiAuthenticationMiddleware, checkHl, (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);
err.status = codes.notfound;
next(err);
return; // prevent node to process this function further after next() has finished.
}
// optional task 3: 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.
AwardingModel.findByIdAndUpdate(req.params.id, req.body, {new: true}, (err, item) => {
if (err) {
err.status = codes.wrongrequest;
}
else if (!item) {
err = new Error("item not found");
err.status = codes.notfound;
}
else {
res.locals.items = item;
}
next(err);
})
})
.delete(apiAuthenticationMiddleware, checkHl, (req, res, next) => { .delete(apiAuthenticationMiddleware, checkHl, (req, res, next) => {
AwardingModel.findByIdAndRemove(req.params.id, (err, item) => { AwardingModel.findByIdAndRemove(req.params.id, (err, item) => {
if (err) { if (err) {

View File

@ -8,11 +8,19 @@ const logger = require('debug')('cc:command');
const codes = require('./http-codes'); const codes = require('./http-codes');
const routerHandling = require('../middleware/router-handling'); const routerHandling = require('../middleware/router-handling');
const createAllSignatures = require('../cron-job/update-signatures').createAllSignatures;
const createSignature = require('../signature-tool/signature-tool'); const createSignature = require('../signature-tool/signature-tool');
const command = express.Router(); const command = express.Router();
// add middleware for bonus tasks 4 and 5 to find filter and offset/limit params for GET / and GET /:id command.route('/createSignature')
.post((req, res, next) => {
createAllSignatures();
})
.all(
routerHandling.httpMethodNotAllowed
);
command.route('/createSignature/:id') command.route('/createSignature/:id')
.post((req, res, next) => { .post((req, res, next) => {

View File

@ -9,6 +9,10 @@ const codes = require('./http-codes');
const routerHandling = require('../middleware/router-handling'); const routerHandling = require('../middleware/router-handling');
const apiAuthenticationMiddleware = require('../middleware/auth-middleware');
const checkSql = require('../middleware/permission-check').checkSql;
const checkHl = require('../middleware/permission-check').checkHl;
// Mongoose Model using mongoDB // Mongoose Model using mongoDB
const UserModel = require('../models/user'); const UserModel = require('../models/user');
const AwardingModel = require('../models/awarding'); const AwardingModel = require('../models/awarding');
@ -31,9 +35,9 @@ const request = express.Router();
// routes ********************** // routes **********************
request.route('/award') request.route('/award')
.post((req, res, next) => { .post(apiAuthenticationMiddleware, checkSql, (req, res, next) => {
const award = new AwardingModel(req.body); const award = new AwardingModel(req.body);
award.confirmed = false; award.confirmed = 0;
award.proposer = req.user._id; award.proposer = req.user._id;
// timestamp and default are set automatically by Mongoose Schema Validation // timestamp and default are set automatically by Mongoose Schema Validation
award.save((err) => { award.save((err) => {
@ -54,18 +58,36 @@ request.route('/award')
request.route('/promotion') request.route('/promotion')
.get((req, res, next) => { .get(apiAuthenticationMiddleware, checkSql, (req, res, next) => {
const squadFilter = req.query.squadId; const squadFilter = req.query.squadId;
const fractFilter = req.query.fractFilter;
const progressFilter = req.query.inProgress;
let filter;
if (squadFilter) {
filter = {squadId: squadFilter};
}
let userIds = []; let userIds = [];
UserModel.find({squadId: squadFilter}, (err, items) => { UserModel.find(filter).populate('squadId').exec((err, items) => {
if (err) { if (err) {
err.status = codes.servererror; err.status = codes.servererror;
return next(err); return next(err);
} }
for (let item of items) { for (let item of items) {
if (!fractFilter || (fractFilter && item.squadId && item.squadId.fraction === fractFilter)) {
userIds.push(item._id); userIds.push(item._id);
} }
PromotionModel.find({userId: {"$in": userIds}}, {}, {sort: {timestamp: 'desc'}}).populate('userId').populate('proposer', resultSet).exec((err, items) => { }
let promotionFilter = {
userId: {"$in": userIds}
};
if (progressFilter) {
promotionFilter.confirmed = 0;
}
PromotionModel.find(promotionFilter, {}, {sort: {timestamp: 'desc'}})
.populate('userId').populate('proposer', resultSet).exec((err, items) => {
if (err) { if (err) {
err.status = codes.servererror; err.status = codes.servererror;
return next(err); return next(err);
@ -83,7 +105,7 @@ request.route('/promotion')
}) })
.post((req, res, next) => { .post(apiAuthenticationMiddleware, checkSql, (req, res, next) => {
const promotion = new PromotionModel(req.body); const promotion = new PromotionModel(req.body);
promotion.confirmed = 0; promotion.confirmed = 0;
promotion.proposer = req.user._id; promotion.proposer = req.user._id;
@ -104,6 +126,54 @@ request.route('/promotion')
routerHandling.httpMethodNotAllowed routerHandling.httpMethodNotAllowed
); );
request.route('/promotion/:id')
.patch(apiAuthenticationMiddleware, checkHl, (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);
err.status = codes.notfound;
next(err);
return; // prevent node to process this function further after next() has finished.
}
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.
PromotionModel.findByIdAndUpdate(req.params.id, req.body, {new: true}, (err, item) => {
if (err) {
err.status = codes.wrongrequest;
}
else if (!item) {
err = new Error("item not found");
err.status = codes.notfound;
}
else {
if (item.confirmed === 1) {
let updateUser = {
_id: item.userId,
rankLvl: item.newRankLvl
};
UserModel.findByIdAndUpdate(updateUser._id, updateUser, {new: true}, (err, item) => {
if (err) {
err.status = codes.wrongrequest;
}
else if (!item) {
err = new Error("user not found");
err.status = codes.notfound;
}
});
}
res.locals.items = item;
}
next(err);
})
})
.all(
routerHandling.httpMethodNotAllowed
);
// this middleware function can be used, if you like or remove it // 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 // 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); request.use(routerHandling.emptyResponse);

View File

@ -17,7 +17,7 @@ const errorResponseWare = require('./middleware/error-response');
const apiAuthenticationMiddleware = require('./middleware/auth-middleware'); const apiAuthenticationMiddleware = require('./middleware/auth-middleware');
const checkSql = require('./middleware/permission-check').checkSql; const checkSql = require('./middleware/permission-check').checkSql;
const checkAdmin = require('./middleware/permission-check').checkAdmin; const checkAdmin = require('./middleware/permission-check').checkAdmin;
const signatureCronJob = require('./cron-job/update-signatures'); const signatureCronJob = require('./cron-job/update-signatures').cronJob;
// router modules // router modules
const authenticateRouter = require('./routes/authenticate'); const authenticateRouter = require('./routes/authenticate');
@ -73,7 +73,7 @@ app.use(urls.users, userRouter);
app.use(urls.squads, squadRouter); app.use(urls.squads, squadRouter);
app.use(urls.ranks, rankRouter); app.use(urls.ranks, rankRouter);
app.use(urls.decorations, decorationRouter); app.use(urls.decorations, decorationRouter);
app.use(urls.request, apiAuthenticationMiddleware, checkSql, requestRouter); app.use(urls.request, requestRouter);
app.use(urls.awards, awardingRouter); app.use(urls.awards, awardingRouter);
app.use(urls.command, apiAuthenticationMiddleware, checkAdmin, commandRouter); app.use(urls.command, apiAuthenticationMiddleware, checkAdmin, commandRouter);
app.use(urls.account, apiAuthenticationMiddleware, checkAdmin, accountRouter); app.use(urls.account, apiAuthenticationMiddleware, checkAdmin, accountRouter);

View File

@ -126,7 +126,7 @@ let addDecorationsAndSave = (userId, loadedImage, res, next) => {
let ribbonPx = 598; let ribbonPx = 598;
let ribbonPy = 95; let ribbonPy = 95;
AwardingModel.find({'userId': userId}, ['decorationId', 'date'], AwardingModel.find({'userId': userId, 'confirmed': 1}, ['decorationId', 'date'],
{sort: {date: 'asc'}}).populate('decorationId', ['isMedal']) {sort: {date: 'asc'}}).populate('decorationId', ['isMedal'])
.exec((err, awardings) => { .exec((err, awardings) => {
if (err) { if (err) {
@ -209,6 +209,8 @@ let saveJimpImageAndCompress = (image, userId, res, next) => {
}).then((files) => { }).then((files) => {
res.locals.items = {status: 'success'}; res.locals.items = {status: 'success'};
return next(); return next();
}).catch((error) => {
console.log(error)
}) })
}, 3000); }, 3000);
}; };

View File

@ -48,6 +48,21 @@
</li> </li>
</ul> </ul>
</li> </li>
<li *ngIf="loginService.hasPermission(2)" class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
aria-expanded="false">
Anträge
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li>
<a [routerLink]="['/confirm-promotion']">Beförderung</a>
</li>
<li>
<a [routerLink]="['/confirm-award']">Orden/ Auszeichnung</a>
</li>
</ul>
</li>
</ul> </ul>
<ul class="nav navbar-nav" style="float: right"> <ul class="nav navbar-nav" style="float: right">

View File

@ -4,7 +4,7 @@ export class AppConfig {
public readonly apiUrl = ''; public readonly apiUrl = '';
public readonly apiAppUserPath = '/account/'; public readonly apiAppUserPath = '/account/';
public readonly apiAwardPath = '/awardings/'; public readonly apiAwardPath = '/awardings';
public readonly apiDecorationPath = '/decorations/'; public readonly apiDecorationPath = '/decorations/';
public readonly apiAuthenticationPath = '/authenticate'; public readonly apiAuthenticationPath = '/authenticate';
public readonly apiSignupPath = '/authenticate/signup'; public readonly apiSignupPath = '/authenticate/signup';

View File

@ -1,7 +1,7 @@
import {Routes, RouterModule} from '@angular/router'; import {RouterModule, Routes} from "@angular/router";
import {LoginComponent} from './login/index'; import {LoginComponent} from "./login/index";
import {NotFoundComponent} from './not-found/not-found.component'; import {NotFoundComponent} from "./not-found/not-found.component";
import {LoginGuardAdmin, LoginGuardHL, LoginGuardSQL} from './login/login.guard'; import {LoginGuardAdmin, LoginGuardHL, LoginGuardSQL} from "./login/login.guard";
import {usersRoutes, usersRoutingComponents} from "./users/users.routing"; import {usersRoutes, usersRoutingComponents} from "./users/users.routing";
import {squadsRoutes, squadsRoutingComponents} from "./squads/squads.routing"; import {squadsRoutes, squadsRoutingComponents} from "./squads/squads.routing";
import {decorationsRoutes, decorationsRoutingComponents} from "./decorations/decoration.routing"; import {decorationsRoutes, decorationsRoutingComponents} from "./decorations/decoration.routing";
@ -11,6 +11,8 @@ 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"; import {RequestPromotionComponent} from "./request/promotion/req-promotion.component";
import {ConfirmPromotionComponent} from "./request/confirm-promotion/confirm-promotion.component";
import {ConfirmAwardComponent} from "./request/confirm-award/confirm-award.component";
export const appRoutes: Routes = [ export const appRoutes: Routes = [
@ -23,6 +25,8 @@ export const appRoutes: Routes = [
{path: 'request-award', component: RequestAwardComponent, canActivate: [LoginGuardSQL]}, {path: 'request-award', component: RequestAwardComponent, canActivate: [LoginGuardSQL]},
{path: 'request-promotion', component: RequestPromotionComponent, canActivate: [LoginGuardSQL]}, {path: 'request-promotion', component: RequestPromotionComponent, canActivate: [LoginGuardSQL]},
{path: 'confirm-award', component: ConfirmAwardComponent, canActivate: [LoginGuardHL]},
{path: 'confirm-promotion', component: ConfirmPromotionComponent, canActivate: [LoginGuardHL]},
{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]},
@ -38,7 +42,8 @@ export const appRoutes: Routes = [
export const appRouting = RouterModule.forRoot(appRoutes); export const appRouting = RouterModule.forRoot(appRoutes);
export const routingComponents = [LoginComponent, SignupComponent, RequestAwardComponent, RequestPromotionComponent, AdminComponent, ...armyRoutingComponents , NotFoundComponent, ...usersRoutingComponents, export const routingComponents = [LoginComponent, SignupComponent, RequestAwardComponent, RequestPromotionComponent, ConfirmAwardComponent,
ConfirmPromotionComponent, AdminComponent, ...armyRoutingComponents, NotFoundComponent, ...usersRoutingComponents,
...squadsRoutingComponents, ...decorationsRoutingComponents, ...ranksRoutingComponents]; ...squadsRoutingComponents, ...decorationsRoutingComponents, ...ranksRoutingComponents];
export const routingProviders = [LoginGuardHL]; export const routingProviders = [LoginGuardHL];

View File

@ -32,7 +32,7 @@
</tr> </tr>
</thead> </thead>
<tbody *ngFor="let award of user.awards"> <tbody *ngFor="let award of user.awards">
<tr *ngIf="award.confirmed" class="cell-outline"> <tr *ngIf="award.confirmed === 1" class="cell-outline">
<td class="text-center" *ngIf="award.decorationId.isMedal"> <td class="text-center" *ngIf="award.decorationId.isMedal">
<img height="90px" src="resource/decoration/{{award.decorationId._id}}.png"> <img height="90px" src="resource/decoration/{{award.decorationId._id}}.png">
</td> </td>

View File

@ -3,9 +3,11 @@
<div class="row"> <div class="row">
<h2 style="text-align: center;" class="form-signin-heading">Registrieren</h2> <h2 style="text-align: center;" class="form-signin-heading">Registrieren</h2>
<p>Dieses Formular nur ausfüllen wenn du einer <b>HL</b> angehörst oder <b>SQL</b> bist. Dabei den Nutzernamen aus dem OPT Forum verwenden! <p>Dieses Formular nur ausfüllen wenn du einer <b>HL</b> angehörst oder <b>SQL</b> bist. Dabei den Nutzernamen aus
Im Forum eine Nachricht an <a href="https://opt-dev.de/dashboard/index.php?conversation-add/&userID=9" target="_blank">HardiReady</a> dem OPT Forum verwenden!
senden, in welcher der 'geheime Text' drin steht, den du bei der Registrierung nutzt.<br> Im Forum eine Nachricht an <a href="https://opt-dev.de/dashboard/index.php?conversation-add/&userID=9"
target="_blank">HardiReady</a>
senden, in welcher der 'geheime Text' steht, den du bei der Registrierung nutzt.<br>
Dabei kann es sich um irgend eine willkürliche Zeichenfolge oder einen Satz handeln - dient nur dem Abgleich. Dabei kann es sich um irgend eine willkürliche Zeichenfolge oder einen Satz handeln - dient nur dem Abgleich.
Anschließend wird dein Account aktiviert und du wirst darüber per PN informiert.</p> Anschließend wird dein Account aktiviert und du wirst darüber per PN informiert.</p>
@ -25,9 +27,16 @@
<span *ngIf="!loading">Registrieren</span> <span *ngIf="!loading">Registrieren</span>
<span *ngIf="loading" class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span> <span *ngIf="loading" class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span>
</button> </button>
<h3 class="text-center">
<span *ngIf="showSuccessLabel"
class="label label-success label-small"
style="margin-left: inherit">
Account erfolgreich erstellt
</span>
</h3>
<span *ngIf="showErrorLabel" <span *ngIf="showErrorLabel"
class="center-block label label-danger" style="font-size: medium; padding: 2px; margin-top: 2px"> class="center-block label label-danger" style="font-size: medium; padding: 2px; margin-top: 2px">
Login fehlgeschlagen {{error}}
</span> </span>
</div> </div>

View File

@ -13,6 +13,10 @@ export class SignupComponent implements OnInit {
showErrorLabel = false; showErrorLabel = false;
showSuccessLabel = false;
error: string;
loading = false; loading = false;
returnUrl: string; returnUrl: string;
@ -35,10 +39,11 @@ export class SignupComponent implements OnInit {
this.loginService.signUp(username, password, secret) this.loginService.signUp(username, password, secret)
.subscribe( .subscribe(
data => { data => {
console.log(data) this.loading = false;
//this.router.navigate([this.returnUrl]); this.showSuccessLabel = true;
}, },
error => { error => {
this.error = error;
this.showErrorLabel = true; this.showErrorLabel = true;
setTimeout(() => { setTimeout(() => {
this.showErrorLabel = false; this.showErrorLabel = false;

View File

@ -37,10 +37,11 @@ export interface Award {
reason?: string; reason?: string;
proposer?: AppUser; proposer?: AppUser;
date?: number; // since Date.now() returns a number date?: number; // since Date.now() returns a number
confirmed?: boolean; confirmed?: number;
} }
export interface Promotion { export interface Promotion {
_id?: string;
userId?: string userId?: string
oldRankLvl: number, oldRankLvl: number,
newRankLvl: number newRankLvl: number

View File

@ -97,10 +97,10 @@
{{award.proposer?.username}} {{award.proposer?.username}}
</td> </td>
<td class="text-right"> <td class="text-right">
<a class="small text-nowrap">{{award.date | date: 'dd.MM.yyyy'}}</a> {{award.date | date: 'dd.MM.yyyy'}}
</td> </td>
<td class="text-center"> <td class="text-center">
{{award.confirmed? 'Bestätigt' : 'In Bearbeitung'}} {{award.confirmed === 0? 'In Bearbeitung' : (award.confirmed === 1? 'Genehmigt' : 'Abgelehnt')}}
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -0,0 +1,43 @@
.decoration-preview {
padding: 5px;
}
.action {
cursor: pointer;
}
.table {
overflow-wrap: break-word;
table-layout: fixed;
}
.table-container {
margin-top: 40px;
overflow-x: auto;
width: 75%;
}
/* 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,55 @@
<form #form="ngForm" class="overview">
<h3>Offene Anträge - Auszeichnungen</h3>
<span *ngIf="showSuccessLabel"
class="label label-success label-small"
style="margin-left: inherit">
Erfolgreich gespeichert
</span>
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th class="col-sm-1">Teilnehmer</th>
<th class="col-sm-1"></th>
<th class="col-sm-2">Auszeichnung</th>
<th class="col-sm-2">Begründung</th>
<th class="col-sm-1 ">Antragsteller</th>
<th class="col-sm-1 text-right">Datum</th>
<th class="col-sm-1 text-right">Aktion</th>
</tr>
</thead>
<tbody *ngFor="let award of awards">
<tr>
<td>
{{award.userId.username}}
</td>
<td *ngIf="award.decorationId.isMedal">
<img height="40px" src="resource/decoration/{{award.decorationId._id}}.png">
</td>
<td *ngIf="!award.decorationId.isMedal">
<img width="60px" src="resource/decoration/{{award.decorationId._id}}.png">
</td>
<td>
{{award.decorationId.name}}
</td>
<td>
{{award.reason}}
</td>
<td>
{{award.proposer?.username}}
</td>
<td class="text-right">
{{award.date | date: 'dd.MM.yyyy'}}
</td>
<td class="text-right">
<a class="action" (click)="confirm(award, true)">Bestätigen</a><br>
<a class="action" (click)="confirm(award, false)">Ablehnen</a>
</td>
</tr>
</tbody>
</table>
</div>
</form>

View File

@ -0,0 +1,49 @@
import {Component} from "@angular/core";
import {ActivatedRoute, Router} from "@angular/router";
import {Award} from "../../models/model-interfaces";
import {AwardingService} from "../../services/awarding-service/awarding.service";
@Component({
templateUrl: './confirm-award.component.html',
styleUrls: ['./confirm-award.component.css'],
})
export class ConfirmAwardComponent {
awards: Award[];
showSuccessLabel = false;
constructor(private router: Router,
private route: ActivatedRoute,
private awardingService: AwardingService) {
}
ngOnInit() {
let currentUser = JSON.parse(localStorage.getItem('currentUser'));
this.awardingService.getUnconfirmedAwards(currentUser.squad.fraction).subscribe(awards => {
this.awards = awards;
});
}
confirm(award: Award, decision: boolean) {
const updateObject = {
_id: award._id,
confirmed: decision ? 1 : 2
};
this.awardingService.updateAward(updateObject).subscribe(res => {
let currentUser = JSON.parse(localStorage.getItem('currentUser'));
this.awardingService.getUnconfirmedAwards(currentUser.squad.fraction).subscribe(awards => {
this.awards = awards;
this.showSuccessLabel = true;
setTimeout(() => {
this.showSuccessLabel = false;
}, 2000);
});
});
}
}

View File

@ -0,0 +1,43 @@
.decoration-preview {
padding: 5px;
}
.action {
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,54 @@
<form #form="ngForm" class="overview">
<h3>Offene Anträge - Beförderung</h3>
<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>
<th class="col-sm-1 text-center">Aktion</th>
</tr>
</thead>
<tbody *ngFor="let promotion of promotions">
<tr>
<td>
{{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>
<td class="text-right">
<a class="action" (click)="confirm(promotion, true)">Bestätigen</a><br>
<a class="action" (click)="confirm(promotion, false)">Ablehnen</a>
</td>
</tr>
</tbody>
</table>
</div>
</form>

View File

@ -0,0 +1,56 @@
import {Component} from "@angular/core";
import {ActivatedRoute, Router} from "@angular/router";
import {Promotion, Rank} from "../../models/model-interfaces";
import {RankService} from "../../services/rank-service/rank.service";
import {PromotionService} from "../../services/promotion-service/promotion.service";
@Component({
templateUrl: './confirm-promotion.component.html',
styleUrls: ['./confirm-promotion.component.css'],
})
export class ConfirmPromotionComponent {
showSuccessLabel = false;
ranks: Rank[];
promotions: Promotion[];
constructor(private router: Router,
private route: ActivatedRoute,
private rankService: RankService,
private promotionService: PromotionService) {
}
ngOnInit() {
let currentUser = JSON.parse(localStorage.getItem('currentUser'));
// show only current users fraction promotions
this.rankService.findRanks('', currentUser.squad.fraction).subscribe(ranks => {
this.ranks = ranks;
});
this.promotionService.getUnconfirmedPromotions(currentUser.squad.fraction).subscribe(promotions => {
this.promotions = promotions;
})
}
confirm(promotion: Promotion, decision: boolean) {
const updateObject = {
_id: promotion._id,
confirmed: decision ? 1 : 2
};
this.promotionService.updatePromotion(updateObject).subscribe(res => {
let currentUser = JSON.parse(localStorage.getItem('currentUser'));
this.promotionService.getUnconfirmedPromotions(currentUser.squad.fraction).subscribe(promotions => {
this.promotions = promotions;
this.showSuccessLabel = true;
setTimeout(() => {
this.showSuccessLabel = false;
}, 2000);
});
});
}
}

View File

@ -1,5 +1,5 @@
import {Injectable} from "@angular/core"; import {Injectable} from "@angular/core";
import {User} from "../../models/model-interfaces"; import {Award, User} from "../../models/model-interfaces";
import {Headers, Http} from "@angular/http"; import {Headers, Http} from "@angular/http";
import {Observable} from "rxjs/Observable"; import {Observable} from "rxjs/Observable";
import {AppConfig} from "../../app.config"; import {AppConfig} from "../../app.config";
@ -12,6 +12,11 @@ export class AwardingService {
private config: AppConfig) { private config: AppConfig) {
} }
getUnconfirmedAwards(fraction?: string) {
return this.http.get(this.config.apiUrl + this.config.apiAwardPath + '?inProgress=true&fractFilter=' + fraction)
.map(res => res.json())
}
/** /**
* get awards array with populated decorations * get awards array with populated decorations
*/ */
@ -20,16 +25,21 @@ export class AwardingService {
.map(res => res.json()) .map(res => res.json())
} }
addAwarding(award) { addAwarding(award: Award) {
return this.http.post(this.config.apiUrl + this.config.apiAwardPath, award) return this.http.post(this.config.apiUrl + this.config.apiAwardPath, award)
} }
requestAwarding(award) { updateAward(award) {
return this.http.patch(this.config.apiUrl + this.config.apiAwardPath + '/' + award._id, award)
.map(res => res.json())
}
requestAwarding(award: Award) {
return this.http.post(this.config.apiUrl + this.config.apiRequestAwardPath, award) return this.http.post(this.config.apiUrl + this.config.apiRequestAwardPath, award)
} }
deleteAwarding(awardingId) { deleteAwarding(awardingId) {
return this.http.delete(this.config.apiUrl + this.config.apiAwardPath + awardingId) return this.http.delete(this.config.apiUrl + this.config.apiAwardPath + '/' + awardingId)
} }
} }

View File

@ -27,11 +27,7 @@ export class LoginService {
signUp(username: string, password: string, secret: string) { signUp(username: string, password: string, secret: string) {
return this.http.post(this.config.apiUrl + this.config.apiSignupPath, {username: username, password: password, secret: secret}) return this.http.post(this.config.apiUrl + this.config.apiSignupPath, {username: username, password: password, secret: secret})
.map((response: Response) => { .map((response: Response) => {
// login successful if there's a jwt token in the response
let user = response.json();
if (user) {
//TODO
}
}); });
} }

View File

@ -10,6 +10,11 @@ export class PromotionService {
private config: AppConfig) { private config: AppConfig) {
} }
getUnconfirmedPromotions(fraction?: string) {
return this.http.get(this.config.apiUrl + this.config.apiPromotionPath + '?inProgress=true&fractFilter=' + fraction)
.map(res => res.json())
}
getSquadPromotions(squadId: string) { getSquadPromotions(squadId: string) {
return this.http.get(this.config.apiUrl + this.config.apiPromotionPath + '?squadId=' + squadId) return this.http.get(this.config.apiUrl + this.config.apiPromotionPath + '?squadId=' + squadId)
.map(res => res.json()) .map(res => res.json())
@ -19,6 +24,11 @@ export class PromotionService {
return this.http.post(this.config.apiUrl + this.config.apiPromotionPath, promotion) return this.http.post(this.config.apiUrl + this.config.apiPromotionPath, promotion)
} }
updatePromotion(promotion) {
return this.http.patch(this.config.apiUrl + this.config.apiPromotionPath + '/' + promotion._id, promotion)
.map(res => res.json())
}
deletePromotion(promotionId) { deletePromotion(promotionId) {
return this.http.delete(this.config.apiUrl + this.config.apiPromotionPath + promotionId) return this.http.delete(this.config.apiUrl + this.config.apiPromotionPath + promotionId)
} }

View File

@ -92,7 +92,7 @@
<a class="small text-nowrap">{{award.date | date: 'dd.MM.yyyy'}}</a> <a class="small text-nowrap">{{award.date | date: 'dd.MM.yyyy'}}</a>
</td> </td>
<td class="text-center"> <td class="text-center">
{{award.confirmed? 'Bestätigt' : 'In Bearbeitung'}} {{award.confirmed === 0? 'In Bearbeitung' : (award.confirmed === 1? 'Genehmigt' : 'Abgelehnt')}}
</td> </td>
<td class="text-center"> <td class="text-center">
<span class="glyphicon glyphicon-trash trash" title="Löschen" (click)="deleteAwarding(award._id)"></span> <span class="glyphicon glyphicon-trash trash" title="Löschen" (click)="deleteAwarding(award._id)"></span>