diff --git a/api/config/api-url.js b/api/config/api-url.js index ee2bb53..4e68b70 100644 --- a/api/config/api-url.js +++ b/api/config/api-url.js @@ -1,18 +1,19 @@ const rootRoute = '/api'; module.exports = { + account: rootRoute + '/account', auth: rootRoute + '/authenticate', - signUp: rootRoute + '/authenticate/signup', awards: rootRoute + '/awardings', - command: rootRoute + '/cmd', + campaigns: rootRoute + '/campaigns', cmdCreateSig: rootRoute + '/cmd/createSignature', + command: rootRoute + '/cmd', decorations: rootRoute + '/decorations', overview: rootRoute + '/overview', ranks: rootRoute + '/ranks', + request: rootRoute + '/request', signatures: '/signatures', + signUp: rootRoute + '/authenticate/signup', squads: rootRoute + '/squads', users: rootRoute + '/users', - account: rootRoute + '/account', - request: rootRoute + '/request', wars: rootRoute + '/wars' }; diff --git a/api/models/campaign.js b/api/models/campaign.js new file mode 100644 index 0000000..405a0c1 --- /dev/null +++ b/api/models/campaign.js @@ -0,0 +1,18 @@ +"use strict"; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const CampaignSchema = new Schema({ + title: { + type: String, + required: true + } +}, { + collection: 'campaign', + timestamps: {createdAt: 'timestamp'} +}); +// optional more indices +CampaignSchema.index({timestamp: 1}); + +module.exports = mongoose.model('Campaign', CampaignSchema); diff --git a/api/models/war.js b/api/models/war.js index ad0cd43..b89bcae 100644 --- a/api/models/war.js +++ b/api/models/war.js @@ -35,6 +35,10 @@ const WarSchema = new Schema({ get: v => Math.round(v), set: v => Math.round(v), default: 0 + }, + campaign: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Campaign' } }, { collection: 'war', diff --git a/api/routes/campaigns.js b/api/routes/campaigns.js new file mode 100644 index 0000000..b095dbf --- /dev/null +++ b/api/routes/campaigns.js @@ -0,0 +1,65 @@ +"use strict"; + +// modules +const express = require('express'); +const logger = require('debug')('cc:campaigns'); + +// HTTP status codes by name +const codes = require('./http-codes'); + +const routerHandling = require('../middleware/router-handling'); +const apiAuthenticationMiddleware = require('../middleware/auth-middleware'); +const checkMT = require('../middleware/permission-check').checkMT; + +// Mongoose Model using mongoDB +const CampaignModel = require('../models/campaign'); + +const campaigns = express.Router(); + +// routes ********************** +campaigns.route('/') + + .post(apiAuthenticationMiddleware, checkMT, (req, res, next) => { + const campaign = new CampaignModel(req.body); + // timestamp and default are set automatically by Mongoose Schema Validation + campaign.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 = campaign; + next(); + }); + }) + + .all( + routerHandling.httpMethodNotAllowed + ); + +campaigns.route('/:id') + .get((req, res, next) => { + CampaignModel.findById(req.params.id, (err, item) => { + if (err) { + err.status = codes.servererror; + return next(err); + } + else if (!item) { + err = new Error("item not found"); + err.status = codes.notfound; + return next(err); + } + res.locals.items = item; + return next(); + }); + }) + .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 +campaigns.use(routerHandling.emptyResponse); + +module.exports = campaigns; diff --git a/api/routes/wars.js b/api/routes/wars.js index 60a9206..e57fcf4 100644 --- a/api/routes/wars.js +++ b/api/routes/wars.js @@ -18,6 +18,7 @@ const checkMT = require('../middleware/permission-check').checkMT; const routerHandling = require('../middleware/router-handling'); // Mongoose Model using mongoDB +const CampaignModel = require('../models/campaign'); const WarModel = require('../models/war'); const PlayerModel = require('../models/player'); @@ -26,20 +27,37 @@ const wars = express.Router(); // routes ********************** wars.route('/') .get((req, res, next) => { - const filter = {}; - WarModel.find(filter, {}, {sort: {date: 'desc'}}, (err, items) => { + let result = []; + CampaignModel.find({}, {}, {sort: {timestamp: 'desc'}}, (err, campaigns) => { if (err) { err.status = codes.servererror; return next(err); } - if (items) { - res.locals.items = items; - } else { - res.locals.items = []; + if (campaigns) { + WarModel.find({}, {}, {sort: {date: 'desc'}}, (err, wars) => { + if (err) { + err.status = codes.servererror; + return next(err); + } + if (wars) { + campaigns.forEach(campaign => { + let entry = {_id: campaign._id, title: campaign.title, wars: []}; + wars.forEach((war) => { + if (String(campaign._id) === String(war.campaign)) { + entry.wars.push(war); + } + }); + result.push(entry); + }); + res.locals.items = result; + } + res.locals.processed = true; + next(); + } + ) + ; } - res.locals.processed = true; - next(); - }); + }) }) .post(apiAuthenticationMiddleware, checkMT, upload.single('log'), (req, res, next) => { @@ -64,7 +82,6 @@ wars.route('/') if (err) { return next(err); } - //TODO: combine run and clean in one script, so log file gets touched only once exec(__dirname + '/../war-parser/run.sh ' + folderName + ' ' + war._id, (error, stdout) => { if (error) { return next(error); @@ -75,7 +92,9 @@ wars.route('/') } let obj = JSON.parse(`${stdout}`); for (let i = 0; i < obj.length; i++) { - if (obj[i].fraction === 'BLUFOR') { + if (!obj[i].fraction) { + obj.splice(i, 1); + } else if (obj[i].fraction === 'BLUFOR') { war.playersBlufor++; } else { war.playersOpfor++; diff --git a/api/server.js b/api/server.js index c51684f..9c203a9 100644 --- a/api/server.js +++ b/api/server.js @@ -32,6 +32,7 @@ const awardingRouter = require('./routes/awardings'); const requestRouter = require('./routes/request'); const signatureRouter = require('./routes/signatures'); const commandRouter = require('./routes/command'); +const campaignRouter = require('./routes/campaigns'); const warRouter = require('./routes/wars'); // Configuration *********************************** @@ -79,6 +80,7 @@ app.use(urls.decorations, decorationRouter); app.use(urls.request, requestRouter); app.use(urls.awards, awardingRouter); app.use(urls.wars, warRouter); +app.use(urls.campaigns,campaignRouter); app.use(urls.command, apiAuthenticationMiddleware, checkAdmin, commandRouter); app.use(urls.account, apiAuthenticationMiddleware, checkAdmin, accountRouter); diff --git a/package.json b/package.json index 58bf657..004bca1 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,11 @@ "start-e2e": "npm run deploy-static && npm run e2e --prefix ./api", "test-e2e": "npm run e2e --prefix ./static" }, + "dependencies": { + "uglify-js": "^3.0.26" + }, "devDependencies": { "concurrently": "^3.4.0", "wait-on": "^2.0.2" - }, - "dependencies": { - "uglify-js": "^3.0.26" } } diff --git a/static/package-lock.json b/static/package-lock.json index 34db079..5f561fb 100644 --- a/static/package-lock.json +++ b/static/package-lock.json @@ -523,12 +523,6 @@ "integrity": "sha1-dMt3+2BS7a/yqJhN2v2I1BnyXKw=", "dev": true }, - "@types/socket.io-client": { - "version": "1.4.29", - "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.29.tgz", - "integrity": "sha1-+HQwcM7pMXXjbgtqd6ivc+WMyzI=", - "dev": true - }, "abbrev": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", @@ -4865,6 +4859,11 @@ "minimist": "0.0.8" } }, + "moment": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", + "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4894,6 +4893,14 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "ngx-bootstrap": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/ngx-bootstrap/-/ngx-bootstrap-1.8.1.tgz", + "integrity": "sha512-f6l1HmyQPY72bvprKd0yt1WkrEFzynbps8+OvEp4KE0beg/UYPoBXAmPyv7sOtn7grOTBUJJcMO147LEqKjW0Q==", + "requires": { + "moment": "2.18.1" + } + }, "ngx-clipboard": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/ngx-clipboard/-/ngx-clipboard-8.0.2.tgz", diff --git a/static/package.json b/static/package.json index 21caec7..6f8dcca 100644 --- a/static/package.json +++ b/static/package.json @@ -30,6 +30,7 @@ "jquery": "^3.1.0", "jquery-ui": "^1.12.0", "jquery-ui-bundle": "^1.11.4", + "ngx-bootstrap": "^1.8.1", "ngx-clipboard": "^8.0.2", "rxjs": "^5.2.0", "ts-helpers": "^1.1.1", diff --git a/static/src/app/app.module.ts b/static/src/app/app.module.ts index b7f158b..51f4444 100644 --- a/static/src/app/app.module.ts +++ b/static/src/app/app.module.ts @@ -35,10 +35,11 @@ import {WarService} from "./services/war-service/war.service"; import {DataTableModule} from "angular2-datatable"; import {NgxChartsModule} from "@swimlane/ngx-charts"; import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; +import {AccordionModule} from "ngx-bootstrap"; @NgModule({ imports: [BrowserModule, FormsModule, ReactiveFormsModule, appRouting, HttpModule, ClipboardModule, DataTableModule, - BrowserAnimationsModule, NgxChartsModule], + BrowserAnimationsModule, NgxChartsModule, AccordionModule.forRoot()], providers: [ HttpClient, LoginService, diff --git a/static/src/app/decorations/decoration-list/decoration-item.component.html b/static/src/app/decorations/decoration-list/decoration-item.component.html index f60ded5..4e8fe41 100644 --- a/static/src/app/decorations/decoration-list/decoration-item.component.html +++ b/static/src/app/decorations/decoration-list/decoration-item.component.html @@ -1,7 +1,7 @@
-
+
{{decoration.name}} @@ -12,7 +12,7 @@ - Sortierung: {{decoration.sortingNumber}}
-
+
-
+
{{rank.name}} @@ -11,7 +11,7 @@ - Stufe {{rank.level}}
-
+
diff --git a/static/src/app/services/war-service/war.service.ts b/static/src/app/services/war-service/war.service.ts index ac6de1a..de761c8 100644 --- a/static/src/app/services/war-service/war.service.ts +++ b/static/src/app/services/war-service/war.service.ts @@ -1,16 +1,18 @@ import {Injectable} from "@angular/core"; -import {War} from "../../models/model-interfaces"; +import {Campaign, War} from "../../models/model-interfaces"; import {AppConfig} from "../../app.config"; import {HttpClient} from "../http-client"; @Injectable() export class WarService { + campaigns :Campaign[]; + constructor(private http: HttpClient, private config: AppConfig) { } - getAllWars() { + getAllCampaigns() { return this.http.get(this.config.apiWarPath) .map(res => res.json()) } diff --git a/static/src/app/squads/squad-list/squad-item.component.html b/static/src/app/squads/squad-list/squad-item.component.html index a1e7aea..43827b6 100644 --- a/static/src/app/squads/squad-list/squad-item.component.html +++ b/static/src/app/squads/squad-list/squad-item.component.html @@ -1,7 +1,7 @@
-
+
{{squad.name}} @@ -10,7 +10,7 @@ NATO
-
+
diff --git a/static/src/app/statistic/overview/stats-overview.component.css b/static/src/app/statistic/overview/stats-overview.component.css index 68d4fa9..ef060c6 100644 --- a/static/src/app/statistic/overview/stats-overview.component.css +++ b/static/src/app/statistic/overview/stats-overview.component.css @@ -1,9 +1,3 @@ -h3 { - width: 920px; - margin-left: 5%; -} - .chart-container { width: 1200px; - margin-left: 5%; } diff --git a/static/src/app/statistic/overview/stats-overview.component.html b/static/src/app/statistic/overview/stats-overview.component.html index ecbb758..d6c6c1d 100644 --- a/static/src/app/statistic/overview/stats-overview.component.html +++ b/static/src/app/statistic/overview/stats-overview.component.html @@ -1,3 +1,5 @@ +

{{title}}

+

Punkte

diff --git a/static/src/app/statistic/overview/stats-overview.component.ts b/static/src/app/statistic/overview/stats-overview.component.ts index a762ee0..138c62e 100644 --- a/static/src/app/statistic/overview/stats-overview.component.ts +++ b/static/src/app/statistic/overview/stats-overview.component.ts @@ -1,14 +1,20 @@ import {Component} from "@angular/core"; import {WarService} from "../../services/war-service/war.service"; +import {Campaign, War} from "../../models/model-interfaces"; +import {WarListComponent} from "../war-list/war-list.component"; +import {ActivatedRoute} from "@angular/router"; @Component({ selector: 'stats-overview', templateUrl: './stats-overview.component.html', - styleUrls: ['./stats-overview.component.css'] + styleUrls: ['./stats-overview.component.css'], + inputs: ['campaigns'] }) export class StatisticOverviewComponent { + title = ""; + pointData: any[] = []; playerData: any[] = []; @@ -16,12 +22,40 @@ export class StatisticOverviewComponent { domain: ['#0000FF', '#B22222'] }; - constructor(private warService: WarService) { + constructor(private route: ActivatedRoute, + private warService: WarService) { } ngOnInit() { - this.warService.getAllWars().subscribe(items => { - this.initChart(items); + this.route.params + .map(params => params['id']) + .subscribe((id) => { + if (this.warService.campaigns) { + this.initWars(this.warService.campaigns, id); + } else { + this.warService.getAllCampaigns().subscribe(campaigns => { + this.initWars(campaigns, id); + }) + } + }); + } + + initWars(campaigns, id) { + + let wars = []; + let itemsProcessed = 0; + campaigns = campaigns.filter(campaign => id === 'all' || campaign._id === id); + campaigns.forEach(campaign => { + if (id === 'all') { + this.title = "Gesamtübersicht" + } else { + this.title = campaign.title + } + wars = wars.concat(campaign.wars); + itemsProcessed++; + if (itemsProcessed === campaigns.length) { + this.initChart(wars); + } }) } diff --git a/static/src/app/statistic/stats.component.css b/static/src/app/statistic/stats.component.css index d9e6a0b..e69de29 100644 --- a/static/src/app/statistic/stats.component.css +++ b/static/src/app/statistic/stats.component.css @@ -1,9 +0,0 @@ -#left { - width: 320px; - float: left; - padding-right: 10px; -} - -#right { - overflow: hidden -} diff --git a/static/src/app/statistic/stats.routing.ts b/static/src/app/statistic/stats.routing.ts index 53d09dc..b34c8d1 100644 --- a/static/src/app/statistic/stats.routing.ts +++ b/static/src/app/statistic/stats.routing.ts @@ -17,7 +17,7 @@ export const statsRoutes: Routes = [{ ] }, { - path: 'overview', + path: 'overview/:id', component: StatisticOverviewComponent, outlet: 'right' }, diff --git a/static/src/app/statistic/war-detail/war-detail.component.html b/static/src/app/statistic/war-detail/war-detail.component.html index 604789f..2719f75 100644 --- a/static/src/app/statistic/war-detail/war-detail.component.html +++ b/static/src/app/statistic/war-detail/war-detail.component.html @@ -1,6 +1,6 @@
-
+

{{war.title}} - vom {{war.date | date: 'dd.MM.yyyy'}}

Endpunktestand:

@@ -9,7 +9,7 @@ {{war.ptOpfor}} CSAT -

+

Teilnehmer:

-
+
Logfile anzeigen
@@ -50,7 +50,7 @@
-
+
diff --git a/static/src/app/statistic/war-list/war-list.component.css b/static/src/app/statistic/war-list/war-list.component.css index c072fcd..3a4d680 100644 --- a/static/src/app/statistic/war-list/war-list.component.css +++ b/static/src/app/statistic/war-list/war-list.component.css @@ -6,3 +6,10 @@ .rank-list { width: 100%; } + +:host /deep/ .card-header { + background-color: slategray; + padding: 15px; + color: white; + font-weight: 600; +} diff --git a/static/src/app/statistic/war-list/war-list.component.html b/static/src/app/statistic/war-list/war-list.component.html index 47bd42f..4a86e4d 100644 --- a/static/src/app/statistic/war-list/war-list.component.html +++ b/static/src/app/statistic/war-list/war-list.component.html @@ -5,20 +5,44 @@ -
+
- - + + +
+ {{campaign.title}} + +
+ +
+
+ +
+
+ +
+ + +
+
+
+
diff --git a/static/src/app/statistic/war-list/war-list.component.ts b/static/src/app/statistic/war-list/war-list.component.ts index a551d8f..ffdb1ad 100644 --- a/static/src/app/statistic/war-list/war-list.component.ts +++ b/static/src/app/statistic/war-list/war-list.component.ts @@ -1,6 +1,6 @@ import {Component, OnInit} from "@angular/core"; import {ActivatedRoute, Router} from "@angular/router"; -import {War} from "../../models/model-interfaces"; +import {Campaign, War} from "../../models/model-interfaces"; import {WarService} from "../../services/war-service/war.service"; import {LoginService} from "../../services/login-service/login-service"; @@ -11,9 +11,9 @@ import {LoginService} from "../../services/login-service/login-service"; }) export class WarListComponent implements OnInit { - selectedWarId: string | number = '0'; + selectedWarId: string | number = 'all'; - wars: War[] = []; + campaigns: Campaign[] = []; constructor(private warService: WarService, private loginService: LoginService, @@ -22,9 +22,10 @@ export class WarListComponent implements OnInit { } ngOnInit() { - this.warService.getAllWars().subscribe((items) => { - this.wars = items; - this.router.navigate([{outlets: {'right': ['overview']}}], {relativeTo: this.route}); + this.warService.getAllCampaigns().subscribe((items) => { + this.warService.campaigns = items; + this.campaigns = items; + this.router.navigate([{outlets: {'right': ['overview', 'all']}}], {relativeTo: this.route}); }); } @@ -33,14 +34,18 @@ export class WarListComponent implements OnInit { this.router.navigate([{outlets: {'right': ['new']}}], {relativeTo: this.route}); } - selectWar(warId: string | number) { - this.selectedWarId = warId; - this.router.navigate([{outlets: {'right': ['war', warId]}}], {relativeTo: this.route}); + selectWar(warId) { + if (this.selectedWarId != warId) { + this.selectedWarId = warId; + this.router.navigate([{outlets: {'right': ['war', warId]}}], {relativeTo: this.route}); + } } - selectOverview() { - this.selectedWarId = '0'; - this.router.navigate([{outlets: {'right': ['overview']}}], {relativeTo: this.route}); + selectOverview(overviewId) { + if (this.selectedWarId != overviewId) { + this.selectedWarId = overviewId; + this.router.navigate([{outlets: {'right': ['overview', overviewId]}}], {relativeTo: this.route}); + } } deleteWar(war: War) { @@ -48,10 +53,11 @@ export class WarListComponent implements OnInit { this.warService.deleteWar(war._id) .subscribe((res) => { if (this.selectedWarId === war._id) { - this.selectOverview(); + this.selectOverview('all'); } - this.wars.splice(this.wars.indexOf(war), 1); + this.campaigns.splice(this.campaigns.indexOf(war), 1); }) } } + } diff --git a/static/src/app/statistic/war-submit/war-submit.component.html b/static/src/app/statistic/war-submit/war-submit.component.html index eee5b13..44fe2c2 100644 --- a/static/src/app/statistic/war-submit/war-submit.component.html +++ b/static/src/app/statistic/war-submit/war-submit.component.html @@ -42,6 +42,19 @@ +
+ + +
+
-
+
{{user.username}} @@ -11,7 +11,7 @@ ohne Squad/Fraktion
-
+
diff --git a/static/src/favicon.ico b/static/src/favicon.ico index c37b232..0a04843 100644 Binary files a/static/src/favicon.ico and b/static/src/favicon.ico differ diff --git a/static/src/styles.css b/static/src/styles.css index 8b09d2d..b8fde6e 100644 --- a/static/src/styles.css +++ b/static/src/styles.css @@ -40,16 +40,14 @@ form { } #left { - width:480px; + width: 20%; + min-width: 350px; + max-width: 450px; float: left; padding-right: 10px; + } #right { - overflow: hidden -} - -.chat-button { - position: absolute; - bottom: 0px; - right: 10px; + overflow: hidden; + padding-left: 4%; }